mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-03-09 17:13:45 -07:00
Compare commits
159 Commits
tests_apwo
...
custom_web
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
62b3fd4d37 | ||
|
|
e2f7153312 | ||
|
|
96d4143030 | ||
|
|
a1dcaf52e3 | ||
|
|
aab8f31345 | ||
|
|
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
9
.github/workflows/build.yml
vendored
@@ -38,12 +38,13 @@ jobs:
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
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"
|
||||
echo "$NAME -> $ZIP_NAME"
|
||||
echo "ZIP_NAME=$ZIP_NAME" >> $Env:GITHUB_ENV
|
||||
New-Item -Path dist -ItemType Directory -Force
|
||||
cd build
|
||||
Rename-Item exe.$NAME Archipelago
|
||||
Rename-Item "exe.$NAME" Archipelago
|
||||
7z a -mx=9 -mhe=on -ms "../dist/$ZIP_NAME" Archipelago
|
||||
- name: Store 7z
|
||||
uses: actions/upload-artifact@v3
|
||||
@@ -65,10 +66,10 @@ jobs:
|
||||
- name: Get a recent python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.9'
|
||||
python-version: '3.11'
|
||||
- name: Install build-time dependencies
|
||||
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
|
||||
chmod a+rx appimagetool-x86_64.AppImage
|
||||
./appimagetool-x86_64.AppImage --appimage-extract
|
||||
|
||||
4
.github/workflows/release.yml
vendored
4
.github/workflows/release.yml
vendored
@@ -44,10 +44,10 @@ jobs:
|
||||
- name: Get a recent python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.9'
|
||||
python-version: '3.11'
|
||||
- name: Install build-time dependencies
|
||||
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
|
||||
chmod a+rx appimagetool-x86_64.AppImage
|
||||
./appimagetool-x86_64.AppImage --appimage-extract
|
||||
|
||||
6
.github/workflows/unittests.yml
vendored
6
.github/workflows/unittests.yml
vendored
@@ -36,12 +36,13 @@ jobs:
|
||||
- {version: '3.8'}
|
||||
- {version: '3.9'}
|
||||
- {version: '3.10'}
|
||||
- {version: '3.11'}
|
||||
include:
|
||||
- python: {version: '3.8'} # win7 compat
|
||||
os: windows-latest
|
||||
- python: {version: '3.10'} # current
|
||||
- python: {version: '3.11'} # current
|
||||
os: windows-latest
|
||||
- python: {version: '3.10'} # current
|
||||
- python: {version: '3.11'} # current
|
||||
os: macos-latest
|
||||
|
||||
steps:
|
||||
@@ -55,6 +56,7 @@ jobs:
|
||||
python -m pip install --upgrade pip
|
||||
pip install pytest pytest-subtests
|
||||
python ModuleUpdate.py --yes --force --append "WebHostLib/requirements.txt"
|
||||
python Launcher.py --update_settings # make sure host.yaml exists for tests
|
||||
- name: Unittests
|
||||
run: |
|
||||
pytest
|
||||
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -37,6 +37,7 @@ README.html
|
||||
EnemizerCLI/
|
||||
/Players/
|
||||
/SNI/
|
||||
/host.yaml
|
||||
/options.yaml
|
||||
/config.yaml
|
||||
/logs/
|
||||
@@ -168,6 +169,10 @@ dmypy.json
|
||||
# Cython debug symbols
|
||||
cython_debug/
|
||||
|
||||
# Cython intermediates
|
||||
_speedups.cpp
|
||||
_speedups.html
|
||||
|
||||
# minecraft server stuff
|
||||
jdk*/
|
||||
minecraft*/
|
||||
|
||||
185
BaseClasses.py
185
BaseClasses.py
@@ -9,7 +9,8 @@ import typing # this can go away when Python 3.8 support is dropped
|
||||
from argparse import Namespace
|
||||
from collections import ChainMap, Counter, deque
|
||||
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 Options
|
||||
@@ -81,6 +82,7 @@ class MultiWorld():
|
||||
|
||||
random: random.Random
|
||||
per_slot_randoms: Dict[int, random.Random]
|
||||
"""Deprecated. Please use `self.random` instead."""
|
||||
|
||||
class AttributeProxy():
|
||||
def __init__(self, rule):
|
||||
@@ -242,6 +244,7 @@ class MultiWorld():
|
||||
setattr(self, option_key, getattr(args, option_key, {}))
|
||||
|
||||
self.worlds[player] = world_type(self, player)
|
||||
self.worlds[player].random = self.per_slot_randoms[player]
|
||||
|
||||
def set_item_links(self):
|
||||
item_links = {}
|
||||
@@ -484,8 +487,10 @@ class MultiWorld():
|
||||
def get_unfilled_locations_for_players(self, location_names: List[str], players: Iterable[int]):
|
||||
for player in players:
|
||||
if not location_names:
|
||||
location_names = [location.name for location in self.get_unfilled_locations(player)]
|
||||
for location_name in location_names:
|
||||
valid_locations = [location.name for location in self.get_unfilled_locations(player)]
|
||||
else:
|
||||
valid_locations = location_names
|
||||
for location_name in valid_locations:
|
||||
location = self._location_cache.get((location_name, player), None)
|
||||
if location is not None and location.item is None:
|
||||
yield location
|
||||
@@ -786,78 +791,6 @@ class CollectionState():
|
||||
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:
|
||||
access_rule: Callable[[CollectionState], bool] = staticmethod(lambda state: True)
|
||||
hide_path: bool = False
|
||||
@@ -896,6 +829,108 @@ class Entrance:
|
||||
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]
|
||||
|
||||
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: 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):
|
||||
DEFAULT = 1
|
||||
PRIORITY = 2
|
||||
|
||||
@@ -833,7 +833,7 @@ async def process_server_cmd(ctx: CommonContext, args: dict):
|
||||
|
||||
elif cmd == "SetReply":
|
||||
ctx.stored_data[args["key"]] = args["value"]
|
||||
if args["key"] == "EnergyLink":
|
||||
if args["key"].startswith("EnergyLink"):
|
||||
ctx.current_energy_link_value = args["value"]
|
||||
if ctx.ui:
|
||||
ctx.ui.set_new_energy_link_value()
|
||||
|
||||
7
Fill.py
7
Fill.py
@@ -51,7 +51,10 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations:
|
||||
items_to_place = [items.pop()
|
||||
for items in reachable_items.values() if items]
|
||||
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(
|
||||
base_state, item_pool + unplaced_items)
|
||||
|
||||
@@ -152,8 +155,8 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations:
|
||||
|
||||
if cleanup_required:
|
||||
# validate all placements and remove invalid ones
|
||||
state = sweep_from_pool(base_state, [])
|
||||
for placement in placements:
|
||||
state = sweep_from_pool(base_state, [])
|
||||
if world.accessibility[placement.item.player] != "minimal" and not placement.can_reach(state):
|
||||
placement.item.location = None
|
||||
unplaced_items.append(placement.item)
|
||||
|
||||
54
Generate.py
54
Generate.py
@@ -14,44 +14,42 @@ import ModuleUpdate
|
||||
|
||||
ModuleUpdate.update()
|
||||
|
||||
import copy
|
||||
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
|
||||
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.AutoWorld import AutoWorldRegister
|
||||
import copy
|
||||
from worlds.generic import PlandoConnection
|
||||
|
||||
|
||||
def mystery_argparse():
|
||||
options = get_options()
|
||||
defaults = options["generator"]
|
||||
|
||||
def resolve_path(path: str, resolver: Callable[[str], str]) -> str:
|
||||
return path if os.path.isabs(path) else resolver(path)
|
||||
options = get_settings()
|
||||
defaults = options.generator
|
||||
|
||||
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')
|
||||
parser.add_argument('--samesettings', help='Rolls settings per weights file rather than per player',
|
||||
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.")
|
||||
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('--spoiler', type=int, default=defaults["spoiler"])
|
||||
parser.add_argument('--outputpath', default=resolve_path(options["general_options"]["output_path"], user_path),
|
||||
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('--outputpath', default=options.general_options.output_path,
|
||||
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('--meta_file_path', default=defaults["meta_file_path"])
|
||||
parser.add_argument('--race', action='store_true', default=defaults.race)
|
||||
parser.add_argument('--meta_file_path', default=defaults.meta_file_path)
|
||||
parser.add_argument('--log_level', default='info', help='Sets log level')
|
||||
parser.add_argument('--yaml_output', default=0, type=lambda value: max(int(value), 0),
|
||||
help='Output rolled mystery results to yaml up to specified number (made for async multiworld)')
|
||||
parser.add_argument('--plando', default=defaults["plando_options"],
|
||||
parser.add_argument('--plando', default=defaults.plando_options,
|
||||
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",
|
||||
help="Skip progression balancing step during generation.")
|
||||
@@ -71,6 +69,8 @@ def get_seed_name(random_source) -> str:
|
||||
def main(args=None, callback=ERmain):
|
||||
if not args:
|
||||
args, options = mystery_argparse()
|
||||
else:
|
||||
options = get_settings()
|
||||
|
||||
seed = get_seed(args.seed)
|
||||
Utils.init_logging(f"Generate_{seed}", loglevel=args.log_level)
|
||||
@@ -86,7 +86,7 @@ def main(args=None, callback=ERmain):
|
||||
try:
|
||||
weights_cache[args.weights_file_path] = read_weights_yamls(args.weights_file_path)
|
||||
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} >> "
|
||||
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:
|
||||
meta_weights = read_weights_yamls(args.meta_file_path)[-1]
|
||||
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)}")
|
||||
try: # meta description allows us to verify that the file named meta.yaml is intentionally a meta file
|
||||
del(meta_weights["meta_description"])
|
||||
@@ -114,7 +114,7 @@ def main(args=None, callback=ERmain):
|
||||
try:
|
||||
weights_cache[fname] = read_weights_yamls(path)
|
||||
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:
|
||||
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.seed = seed
|
||||
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.race = args.race
|
||||
erargs.outputname = seed_name
|
||||
@@ -195,7 +195,7 @@ def main(args=None, callback=ERmain):
|
||||
|
||||
player += 1
|
||||
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:
|
||||
raise RuntimeError(f'No weights specified for player {player}')
|
||||
|
||||
@@ -374,7 +374,7 @@ def roll_linked_options(weights: dict) -> dict:
|
||||
else:
|
||||
logging.debug(f"linked option {option_set['name']} skipped.")
|
||||
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
|
||||
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"])
|
||||
|
||||
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
|
||||
return weights
|
||||
|
||||
|
||||
23
Launcher.py
23
Launcher.py
@@ -22,6 +22,7 @@ from shutil import which
|
||||
from typing import Sequence, Union, Optional
|
||||
|
||||
import Utils
|
||||
import settings
|
||||
from worlds.LauncherComponents import Component, components, Type, SuffixIdentifier, icon_paths
|
||||
|
||||
if __name__ == "__main__":
|
||||
@@ -33,7 +34,8 @@ from Utils import is_frozen, user_path, local_path, init_logging, open_filename,
|
||||
|
||||
|
||||
def open_host_yaml():
|
||||
file = user_path('host.yaml')
|
||||
file = settings.get_settings().filename
|
||||
assert file, "host.yaml missing"
|
||||
if is_linux:
|
||||
exe = which('sensible-editor') or which('gedit') or \
|
||||
which('xdg-open') or which('gnome-open') or which('kde-open')
|
||||
@@ -84,6 +86,11 @@ def open_folder(folder_path):
|
||||
webbrowser.open(folder_path)
|
||||
|
||||
|
||||
def update_settings():
|
||||
from settings import get_settings
|
||||
get_settings().save()
|
||||
|
||||
|
||||
components.extend([
|
||||
# Functions
|
||||
Component("Open host.yaml", func=open_host_yaml),
|
||||
@@ -256,11 +263,13 @@ def main(args: Optional[Union[argparse.Namespace, dict]] = None):
|
||||
if not component:
|
||||
logging.warning(f"Could not identify Component responsible for {args['Patch|Game|Component']}")
|
||||
|
||||
if args["update_settings"]:
|
||||
update_settings()
|
||||
if 'file' in args:
|
||||
run_component(args["component"], args["file"], *args["args"])
|
||||
elif 'component' in args:
|
||||
run_component(args["component"], *args["args"])
|
||||
else:
|
||||
elif not args["update_settings"]:
|
||||
run_gui()
|
||||
|
||||
|
||||
@@ -269,9 +278,13 @@ if __name__ == '__main__':
|
||||
Utils.freeze_support()
|
||||
multiprocessing.set_start_method("spawn") # if launched process uses kivy, fork won't work
|
||||
parser = argparse.ArgumentParser(description='Archipelago Launcher')
|
||||
parser.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.")
|
||||
parser.add_argument('args', nargs="*", help="Arguments to pass to component.")
|
||||
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.")
|
||||
run_group.add_argument("args", nargs="*",
|
||||
help="Arguments to pass to component.")
|
||||
main(parser.parse_args())
|
||||
|
||||
from worlds.LauncherComponents import processes
|
||||
|
||||
@@ -9,16 +9,19 @@ if __name__ == "__main__":
|
||||
import asyncio
|
||||
import base64
|
||||
import binascii
|
||||
import colorama
|
||||
import io
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import select
|
||||
import shlex
|
||||
import socket
|
||||
import struct
|
||||
import sys
|
||||
import subprocess
|
||||
import time
|
||||
import typing
|
||||
import urllib
|
||||
|
||||
import colorama
|
||||
import struct
|
||||
|
||||
from CommonClient import (CommonContext, get_base_parser, gui_enabled, logger,
|
||||
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.Tracker import LocationTracker, MagpieBridge
|
||||
|
||||
|
||||
class GameboyException(Exception):
|
||||
pass
|
||||
|
||||
@@ -115,17 +119,17 @@ class RAGameboy():
|
||||
assert (self.socket)
|
||||
self.socket.setblocking(False)
|
||||
|
||||
def get_retroarch_version(self):
|
||||
self.send(b'VERSION\n')
|
||||
select.select([self.socket], [], [])
|
||||
response_str, addr = self.socket.recvfrom(16)
|
||||
async def send_command(self, command, timeout=1.0):
|
||||
self.send(f'{command}\n')
|
||||
response_str = await self.async_recv()
|
||||
self.check_command_response(command, response_str)
|
||||
return response_str.rstrip()
|
||||
|
||||
def get_retroarch_status(self, timeout):
|
||||
self.send(b'GET_STATUS\n')
|
||||
select.select([self.socket], [], [], timeout)
|
||||
response_str, addr = self.socket.recvfrom(1000, )
|
||||
return response_str.rstrip()
|
||||
async def get_retroarch_version(self):
|
||||
return await self.send_command("VERSION")
|
||||
|
||||
async def get_retroarch_status(self):
|
||||
return await self.send_command("GET_STATUS")
|
||||
|
||||
def set_cache_limits(self, cache_start, cache_size):
|
||||
self.cache_start = cache_start
|
||||
@@ -141,8 +145,8 @@ class RAGameboy():
|
||||
response, _ = self.socket.recvfrom(4096)
|
||||
return response
|
||||
|
||||
async def async_recv(self):
|
||||
response = await asyncio.get_event_loop().sock_recv(self.socket, 4096)
|
||||
async def async_recv(self, timeout=1.0):
|
||||
response = await asyncio.wait_for(asyncio.get_event_loop().sock_recv(self.socket, 4096), timeout)
|
||||
return response
|
||||
|
||||
async def check_safe_gameplay(self, throw=True):
|
||||
@@ -169,6 +173,8 @@ class RAGameboy():
|
||||
raise InvalidEmulatorStateError()
|
||||
return False
|
||||
if not await check_wram():
|
||||
if throw:
|
||||
raise InvalidEmulatorStateError()
|
||||
return False
|
||||
return True
|
||||
|
||||
@@ -227,20 +233,30 @@ class RAGameboy():
|
||||
|
||||
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):
|
||||
command = "READ_CORE_MEMORY"
|
||||
|
||||
self.send(f'{command} {hex(address)} {size}\n')
|
||||
response = self.recv()
|
||||
|
||||
self.check_command_response(command, response)
|
||||
|
||||
splits = response.decode().split(" ", 2)
|
||||
|
||||
assert (splits[0] == command)
|
||||
# Ignore the address for now
|
||||
|
||||
# TODO: transform to bytes
|
||||
if splits[2][:2] == "-1" or splits[0] != "READ_CORE_MEMORY":
|
||||
if splits[2][:2] == "-1":
|
||||
raise BadRetroArchResponse()
|
||||
|
||||
# TODO: check response address, check hex behavior between RA and BH
|
||||
|
||||
return bytearray.fromhex(splits[2])
|
||||
|
||||
async def async_read_memory(self, address, size=1):
|
||||
@@ -248,14 +264,21 @@ class RAGameboy():
|
||||
|
||||
self.send(f'{command} {hex(address)} {size}\n')
|
||||
response = await self.async_recv()
|
||||
self.check_command_response(command, response)
|
||||
response = response[:-1]
|
||||
splits = response.decode().split(" ", 2)
|
||||
try:
|
||||
response_addr = int(splits[1], 16)
|
||||
except ValueError:
|
||||
raise BadRetroArchResponse()
|
||||
|
||||
assert (splits[0] == command)
|
||||
# Ignore the address for now
|
||||
if response_addr != address:
|
||||
raise BadRetroArchResponse()
|
||||
|
||||
# TODO: transform to bytes
|
||||
return bytearray.fromhex(splits[2])
|
||||
ret = bytearray.fromhex(splits[2])
|
||||
if len(ret) > size:
|
||||
raise BadRetroArchResponse()
|
||||
return ret
|
||||
|
||||
def write_memory(self, address, bytes):
|
||||
command = "WRITE_CORE_MEMORY"
|
||||
@@ -263,7 +286,7 @@ class RAGameboy():
|
||||
self.send(f'{command} {hex(address)} {" ".join(hex(b) for b in bytes)}')
|
||||
select.select([self.socket], [], [])
|
||||
response, _ = self.socket.recvfrom(4096)
|
||||
|
||||
self.check_command_response(command, response)
|
||||
splits = response.decode().split(" ", 3)
|
||||
|
||||
assert (splits[0] == command)
|
||||
@@ -281,6 +304,9 @@ class LinksAwakeningClient():
|
||||
pending_deathlink = False
|
||||
deathlink_debounce = True
|
||||
recvd_checks = {}
|
||||
retroarch_address = None
|
||||
retroarch_port = None
|
||||
gameboy = None
|
||||
|
||||
def msg(self, m):
|
||||
logger.info(m)
|
||||
@@ -288,50 +314,48 @@ class LinksAwakeningClient():
|
||||
self.gameboy.send(s)
|
||||
|
||||
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):
|
||||
logger.info("Waiting on connection to Retroarch...")
|
||||
if not self.stop_bizhawk_spam:
|
||||
logger.info("Waiting on connection to Retroarch...")
|
||||
self.stop_bizhawk_spam = True
|
||||
self.gameboy = RAGameboy(self.retroarch_address, self.retroarch_port)
|
||||
|
||||
while True:
|
||||
try:
|
||||
version = self.gameboy.get_retroarch_version()
|
||||
version = await self.gameboy.get_retroarch_version()
|
||||
NO_CONTENT = b"GET_STATUS CONTENTLESS"
|
||||
status = NO_CONTENT
|
||||
core_type = None
|
||||
GAME_BOY = b"game_boy"
|
||||
while status == NO_CONTENT or core_type != GAME_BOY:
|
||||
try:
|
||||
status = self.gameboy.get_retroarch_status(0.1)
|
||||
if status.count(b" ") < 2:
|
||||
await asyncio.sleep(1.0)
|
||||
continue
|
||||
|
||||
GET_STATUS, PLAYING, info = status.split(b" ", 2)
|
||||
if status.count(b",") < 2:
|
||||
await asyncio.sleep(1.0)
|
||||
continue
|
||||
core_type, rom_name, self.game_crc = info.split(b",", 2)
|
||||
if core_type != GAME_BOY:
|
||||
logger.info(
|
||||
f"Core type should be '{GAME_BOY}', found {core_type} instead - wrong type of ROM?")
|
||||
await asyncio.sleep(1.0)
|
||||
continue
|
||||
except (BlockingIOError, TimeoutError) as e:
|
||||
await asyncio.sleep(0.1)
|
||||
pass
|
||||
logger.info(f"Connected to Retroarch {version} {info}")
|
||||
self.gameboy.read_memory(0x1000)
|
||||
status = await self.gameboy.get_retroarch_status()
|
||||
if status.count(b" ") < 2:
|
||||
await asyncio.sleep(1.0)
|
||||
continue
|
||||
GET_STATUS, PLAYING, info = status.split(b" ", 2)
|
||||
if status.count(b",") < 2:
|
||||
await asyncio.sleep(1.0)
|
||||
continue
|
||||
core_type, rom_name, self.game_crc = info.split(b",", 2)
|
||||
if core_type != GAME_BOY:
|
||||
logger.info(
|
||||
f"Core type should be '{GAME_BOY}', found {core_type} instead - wrong type of ROM?")
|
||||
await asyncio.sleep(1.0)
|
||||
continue
|
||||
self.stop_bizhawk_spam = False
|
||||
logger.info(f"Connected to Retroarch {version.decode('ascii')} running {rom_name.decode('ascii')}")
|
||||
return
|
||||
except ConnectionResetError:
|
||||
except (BlockingIOError, TimeoutError, ConnectionResetError):
|
||||
await asyncio.sleep(1.0)
|
||||
pass
|
||||
|
||||
def reset_auth(self):
|
||||
auth = binascii.hexlify(self.gameboy.read_memory(0x0134, 12)).decode()
|
||||
|
||||
if self.auth:
|
||||
assert (auth == self.auth)
|
||||
|
||||
async def reset_auth(self):
|
||||
auth = binascii.hexlify(await self.gameboy.async_read_memory(0x0134, 12)).decode()
|
||||
self.auth = auth
|
||||
|
||||
async def wait_and_init_tracker(self):
|
||||
@@ -367,11 +391,14 @@ class LinksAwakeningClient():
|
||||
status = self.gameboy.write_memory(LAClientConstants.wLinkStatusBits, [status])
|
||||
self.gameboy.write_memory(LAClientConstants.wRecvIndex, struct.pack(">H", next_index))
|
||||
|
||||
should_reset_auth = False
|
||||
async def wait_for_game_ready(self):
|
||||
logger.info("Waiting on game to be in valid state...")
|
||||
while not await self.gameboy.check_safe_gameplay(throw=False):
|
||||
pass
|
||||
logger.info("Ready!")
|
||||
if self.should_reset_auth:
|
||||
self.should_reset_auth = False
|
||||
raise GameboyException("Resetting due to wrong archipelago server")
|
||||
logger.info("Game connection ready!")
|
||||
|
||||
async def is_victory(self):
|
||||
return (await self.gameboy.read_memory_cache([LAClientConstants.wGameplayType]))[LAClientConstants.wGameplayType] == 1
|
||||
@@ -398,7 +425,7 @@ class LinksAwakeningClient():
|
||||
if await self.is_victory():
|
||||
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
|
||||
if recv_index in self.recvd_checks:
|
||||
@@ -480,6 +507,15 @@ class LinksAwakeningContext(CommonContext):
|
||||
message = [{"cmd": 'LocationChecks', "locations": self.found_checks}]
|
||||
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
|
||||
async def send_deathlink(self):
|
||||
if self.ENABLE_DEATHLINK:
|
||||
@@ -511,8 +547,17 @@ class LinksAwakeningContext(CommonContext):
|
||||
async def server_auth(self, password_requested: bool = False):
|
||||
if password_requested and not self.password:
|
||||
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
|
||||
await self.get_username()
|
||||
await self.send_connect()
|
||||
|
||||
def on_package(self, cmd: str, args: dict):
|
||||
@@ -520,9 +565,13 @@ class LinksAwakeningContext(CommonContext):
|
||||
self.game = self.slot_info[self.slot].game
|
||||
# TODO - use watcher_event
|
||||
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
|
||||
|
||||
async def sync(self):
|
||||
sync_msg = [{'cmd': 'Sync'}]
|
||||
await self.send_msgs(sync_msg)
|
||||
|
||||
item_id_lookup = get_locations_to_id()
|
||||
|
||||
async def run_game_loop(self):
|
||||
@@ -539,17 +588,31 @@ class LinksAwakeningContext(CommonContext):
|
||||
|
||||
if self.magpie_enabled:
|
||||
self.magpie_task = asyncio.create_task(self.magpie.serve())
|
||||
|
||||
|
||||
# yield to allow UI to start
|
||||
await asyncio.sleep(0)
|
||||
|
||||
while True:
|
||||
try:
|
||||
# TODO: cancel all client tasks
|
||||
logger.info("(Re)Starting game loop")
|
||||
if not self.client.stop_bizhawk_spam:
|
||||
logger.info("(Re)Starting game loop")
|
||||
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()
|
||||
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()
|
||||
|
||||
while True:
|
||||
@@ -560,39 +623,59 @@ class LinksAwakeningContext(CommonContext):
|
||||
self.last_resend = now
|
||||
await self.send_checks()
|
||||
if self.magpie_enabled:
|
||||
self.magpie.set_checks(self.client.tracker.all_checks)
|
||||
await self.magpie.set_item_tracker(self.client.item_tracker)
|
||||
await self.magpie.send_gps(self.client.gps_tracker)
|
||||
try:
|
||||
self.magpie.set_checks(self.client.tracker.all_checks)
|
||||
await self.magpie.set_item_tracker(self.client.item_tracker)
|
||||
await self.magpie.send_gps(self.client.gps_tracker)
|
||||
except Exception:
|
||||
# Don't let magpie errors take out the client
|
||||
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)
|
||||
|
||||
except GameboyException:
|
||||
time.sleep(1.0)
|
||||
pass
|
||||
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():
|
||||
parser = get_base_parser(description="Link's Awakening Client.")
|
||||
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('diff_file', default="", type=str, nargs="?",
|
||||
help='Path to a .apladx Archipelago Binary Patch file')
|
||||
|
||||
|
||||
args = parser.parse_args()
|
||||
logger.info(args)
|
||||
|
||||
if args.diff_file:
|
||||
import Patch
|
||||
logger.info("patch file was supplied - creating rom...")
|
||||
meta, rom_file = Patch.create_rom_file(args.diff_file)
|
||||
if "server" in meta:
|
||||
args.url = meta["server"]
|
||||
if "server" in meta and not args.connect:
|
||||
args.connect = meta["server"]
|
||||
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)
|
||||
|
||||
@@ -604,6 +687,10 @@ async def main():
|
||||
ctx.run_gui()
|
||||
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.shutdown()
|
||||
|
||||
|
||||
@@ -23,9 +23,10 @@ from urllib.request import urlopen
|
||||
import ModuleUpdate
|
||||
ModuleUpdate.update()
|
||||
|
||||
from worlds.alttp.Rom import Sprite, LocalRom, apply_rom_settings, get_base_rom_bytes
|
||||
from worlds.alttp.Rom import LocalRom, apply_rom_settings, get_base_rom_bytes
|
||||
from worlds.alttp.Sprites import Sprite
|
||||
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"
|
||||
@@ -43,6 +44,47 @@ class ArgumentDefaultsHelpFormatter(argparse.RawTextHelpFormatter):
|
||||
def _get_help_string(self, action):
|
||||
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:
|
||||
parser = argparse.ArgumentParser(formatter_class=ArgumentDefaultsHelpFormatter)
|
||||
@@ -52,6 +94,8 @@ def get_argparser() -> argparse.ArgumentParser:
|
||||
help='Path to an ALttP Japan(1.0) rom to use as a base.')
|
||||
parser.add_argument('--loglevel', default='info', const='info', nargs='?',
|
||||
choices=['error', 'info', 'warning', 'debug'], help='Select level of logging for output.')
|
||||
parser.add_argument('--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='?',
|
||||
choices=['normal', 'instant', 'double', 'triple', 'quadruple', 'half'],
|
||||
help='''\
|
||||
@@ -61,7 +105,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('--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('--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='?',
|
||||
choices=['normal', 'hide_goal', 'hide_required', 'hide_both'],
|
||||
help='''\
|
||||
@@ -104,21 +148,23 @@ def get_argparser() -> argparse.ArgumentParser:
|
||||
Alternatively, can be a ALttP Rom patched with a Link
|
||||
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='''\
|
||||
Path to a sound effect to replace Link's "oof" sound.
|
||||
Needs to be in a .brr format and have a length of no
|
||||
more than 2673 bytes, created from a 16-bit signed PCM
|
||||
.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.')
|
||||
return parser
|
||||
|
||||
|
||||
def main():
|
||||
parser = get_argparser()
|
||||
args = parser.parse_args()
|
||||
args.music = not args.disablemusic
|
||||
args = parser.parse_args(namespace=get_adjuster_settings_no_defaults(GAME_ALTTP))
|
||||
|
||||
# set up logger
|
||||
loglevel = {'error': logging.ERROR, 'info': logging.INFO, 'warning': logging.WARNING, 'debug': logging.DEBUG}[
|
||||
args.loglevel]
|
||||
@@ -530,9 +576,6 @@ class AttachTooltip(object):
|
||||
|
||||
def get_rom_frame(parent=None):
|
||||
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)
|
||||
baseRomLabel = Label(romFrame, text='LttP Base Rom: ')
|
||||
@@ -560,33 +603,8 @@ def get_rom_frame(parent=None):
|
||||
|
||||
return romFrame, romVar
|
||||
|
||||
|
||||
def get_rom_options_frame(parent=None):
|
||||
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.columnconfigure(0, weight=1)
|
||||
|
||||
@@ -71,6 +71,7 @@ class MMBN3Context(CommonContext):
|
||||
self.auth_name = None
|
||||
self.slot_data = dict()
|
||||
self.patching_error = False
|
||||
self.sent_hints = []
|
||||
|
||||
async def server_auth(self, password_requested: bool = False):
|
||||
if password_requested and not self.password:
|
||||
@@ -175,13 +176,16 @@ async def parse_payload(payload: dict, ctx: MMBN3Context, force: bool):
|
||||
|
||||
# If trade hinting is enabled, send scout checks
|
||||
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"])]
|
||||
await ctx.send_msgs([{
|
||||
"cmd": "LocationScouts",
|
||||
"locations": scouted_locs,
|
||||
"create_as_hint": 2
|
||||
}])
|
||||
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([{
|
||||
"cmd": "LocationScouts",
|
||||
"locations": scouted_locs,
|
||||
"create_as_hint": 2
|
||||
}])
|
||||
|
||||
|
||||
def check_location_packet(location, memory):
|
||||
|
||||
62
Main.py
62
Main.py
@@ -7,31 +7,24 @@ import tempfile
|
||||
import time
|
||||
import zipfile
|
||||
import zlib
|
||||
from typing import Dict, List, Optional, Set, Tuple
|
||||
from typing import Dict, List, Optional, Set, Tuple, Union
|
||||
|
||||
import worlds
|
||||
from BaseClasses import CollectionState, Item, Location, LocationProgressType, MultiWorld, Region
|
||||
from Fill import balance_multiworld_progression, distribute_items_restrictive, distribute_planned, flood_items
|
||||
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.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
|
||||
|
||||
__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):
|
||||
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:
|
||||
os.makedirs(args.outputpath, exist_ok=True)
|
||||
output_path.cached_path = args.outputpath
|
||||
@@ -140,12 +133,6 @@ 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 -= 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")
|
||||
|
||||
for player in world.player_ids:
|
||||
@@ -154,6 +141,13 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
||||
for location_name in world.priority_locations[player].value:
|
||||
world.get_location(location_name, player).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")
|
||||
|
||||
# remove starting inventory from pool items.
|
||||
@@ -313,35 +307,6 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
||||
er_hint_data: Dict[int, Dict[int, str]] = {}
|
||||
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():
|
||||
import NetUtils
|
||||
slot_data = {}
|
||||
@@ -401,10 +366,11 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
||||
for game_world in world.worlds.values()
|
||||
}
|
||||
|
||||
checks_in_area: Dict[int, Dict[str, Union[int, List[int]]]] = {}
|
||||
|
||||
multidata = {
|
||||
"slot_data": slot_data,
|
||||
"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()},
|
||||
"locations": locations_data,
|
||||
"checks_in_area": checks_in_area,
|
||||
|
||||
@@ -299,7 +299,7 @@ if __name__ == '__main__':
|
||||
|
||||
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"]
|
||||
forge_version = args.forge or versions["forge"]
|
||||
java_version = args.java or versions["java"]
|
||||
|
||||
@@ -38,7 +38,7 @@ import NetUtils
|
||||
import Utils
|
||||
from Utils import version_tuple, restricted_loads, Version, async_start
|
||||
from NetUtils import Endpoint, ClientStatus, NetworkItem, decode, encode, NetworkPlayer, Permission, NetworkSlot, \
|
||||
SlotType
|
||||
SlotType, LocationStore
|
||||
|
||||
min_client_version = Version(0, 1, 6)
|
||||
colorama.init()
|
||||
@@ -152,7 +152,9 @@ class Context:
|
||||
"compatibility": int}
|
||||
# team -> slot id -> list of clients authenticated to slot.
|
||||
clients: typing.Dict[int, typing.Dict[int, typing.List[Client]]]
|
||||
locations: typing.Dict[int, typing.Dict[int, typing.Tuple[int, int, 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]]
|
||||
save_version = 2
|
||||
stored_data: typing.Dict[str, object]
|
||||
@@ -187,8 +189,6 @@ class Context:
|
||||
self.player_name_lookup: typing.Dict[str, team_slot] = {}
|
||||
self.connect_names = {} # names of slots clients can connect to
|
||||
self.allow_releases = {}
|
||||
# player location_id item_id target_player_id
|
||||
self.locations = {}
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.server_password = server_password
|
||||
@@ -284,6 +284,7 @@ class Context:
|
||||
except websockets.ConnectionClosed:
|
||||
logging.exception(f"Exception during send_msgs, could not send {msg}")
|
||||
await self.disconnect(endpoint)
|
||||
return False
|
||||
else:
|
||||
if self.log_network:
|
||||
logging.info(f"Outgoing message: {msg}")
|
||||
@@ -297,6 +298,7 @@ class Context:
|
||||
except websockets.ConnectionClosed:
|
||||
logging.exception("Exception during send_encoded_msgs")
|
||||
await self.disconnect(endpoint)
|
||||
return False
|
||||
else:
|
||||
if self.log_network:
|
||||
logging.info(f"Outgoing message: {msg}")
|
||||
@@ -311,6 +313,7 @@ class Context:
|
||||
websockets.broadcast(sockets, msg)
|
||||
except RuntimeError:
|
||||
logging.exception("Exception during broadcast_send_encoded_msgs")
|
||||
return False
|
||||
else:
|
||||
if self.log_network:
|
||||
logging.info(f"Outgoing broadcast: {msg}")
|
||||
@@ -413,7 +416,7 @@ class Context:
|
||||
self.seed_name = decoded_obj["seed_name"]
|
||||
self.random.seed(self.seed_name)
|
||||
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']
|
||||
for slot, data in self.slot_data.items():
|
||||
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(
|
||||
f"{ctx.get_aliased_name(client.team, client.slot)} (Team #{client.team + 1}) "
|
||||
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})
|
||||
ctx.notify_client(client, "Now that you are connected, "
|
||||
"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):
|
||||
"""register any locations that are in the multidata, pointing towards this player"""
|
||||
all_locations = collections.defaultdict(set)
|
||||
for source_slot, location_data in ctx.locations.items():
|
||||
for location_id, values in location_data.items():
|
||||
if values[1] == slot:
|
||||
all_locations[source_slot].add(location_id)
|
||||
all_locations = ctx.locations.get_for_player(slot)
|
||||
|
||||
ctx.broadcast_text_all("%s (Team #%d) has collected their items from other worlds."
|
||||
% (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]:
|
||||
items = []
|
||||
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)
|
||||
return ctx.locations.get_remaining(ctx.location_checks, team, slot)
|
||||
|
||||
|
||||
def send_items_to(ctx: Context, team: int, target_slot: int, *items: NetworkItem):
|
||||
@@ -977,13 +972,12 @@ def collect_hints(ctx: Context, team: int, slot: int, item: typing.Union[int, st
|
||||
slots.add(group_id)
|
||||
|
||||
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 location_id, (item_id, receiving_player, item_flags) in check_data.items():
|
||||
if receiving_player in slots and item_id == seeked_item_id:
|
||||
found = location_id in ctx.location_checks[team, finding_player]
|
||||
entrance = ctx.er_hint_data.get(finding_player, {}).get(location_id, "")
|
||||
hints.append(NetUtils.Hint(receiving_player, finding_player, location_id, item_id, found, entrance,
|
||||
item_flags))
|
||||
for finding_player, location_id, item_id, receiving_player, item_flags \
|
||||
in ctx.locations.find_item(slots, seeked_item_id):
|
||||
found = location_id in ctx.location_checks[team, finding_player]
|
||||
entrance = ctx.er_hint_data.get(finding_player, {}).get(location_id, "")
|
||||
hints.append(NetUtils.Hint(receiving_player, finding_player, location_id, item_id, found, entrance,
|
||||
item_flags))
|
||||
|
||||
return hints
|
||||
|
||||
@@ -1555,15 +1549,11 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
||||
|
||||
|
||||
def get_checked_checks(ctx: Context, team: int, slot: int) -> typing.List[int]:
|
||||
return [location_id for
|
||||
location_id in ctx.locations[slot] if
|
||||
location_id in ctx.location_checks[team, slot]]
|
||||
return ctx.locations.get_checked(ctx.location_checks, team, slot)
|
||||
|
||||
|
||||
def get_missing_checks(ctx: Context, team: int, slot: int) -> typing.List[int]:
|
||||
return [location_id for
|
||||
location_id in ctx.locations[slot] if
|
||||
location_id not in ctx.location_checks[team, slot]]
|
||||
return ctx.locations.get_missing(ctx.location_checks, team, slot)
|
||||
|
||||
|
||||
def get_client_points(ctx: Context, client: Client) -> int:
|
||||
@@ -2128,13 +2118,15 @@ class ServerCommandProcessor(CommonCommandProcessor):
|
||||
async def console(ctx: Context):
|
||||
import sys
|
||||
queue = asyncio.Queue()
|
||||
Utils.stream_input(sys.stdin, queue)
|
||||
worker = Utils.stream_input(sys.stdin, queue)
|
||||
while not ctx.exit_event.is_set():
|
||||
try:
|
||||
# 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.
|
||||
while queue.qsize() == 0:
|
||||
await asyncio.sleep(0.05)
|
||||
if not worker.is_alive():
|
||||
return
|
||||
input_text = await queue.get()
|
||||
queue.task_done()
|
||||
ctx.commandprocessor(input_text)
|
||||
@@ -2145,7 +2137,7 @@ async def console(ctx: Context):
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
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('--host', default=defaults["host"])
|
||||
parser.add_argument('--port', default=defaults["port"], type=int)
|
||||
@@ -2254,12 +2246,15 @@ async def main(args: argparse.Namespace):
|
||||
if not isinstance(e, ImportError):
|
||||
logging.error(f"Failed to load tkinter ({e})")
|
||||
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
|
||||
|
||||
if not data_filename:
|
||||
logging.info("No file selected. Exiting.")
|
||||
exit(1)
|
||||
import sys
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
ctx.load(data_filename, args.use_embedded_options)
|
||||
|
||||
75
NetUtils.py
75
NetUtils.py
@@ -2,6 +2,7 @@ from __future__ import annotations
|
||||
|
||||
import typing
|
||||
import enum
|
||||
import warnings
|
||||
from json import JSONEncoder, JSONDecoder
|
||||
|
||||
import websockets
|
||||
@@ -343,3 +344,77 @@ class Hint(typing.NamedTuple):
|
||||
@property
|
||||
def local(self):
|
||||
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:
|
||||
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'
|
||||
# Load vanilla ROM, patch file, compress ROM
|
||||
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)
|
||||
|
||||
sub_file = None
|
||||
|
||||
21
Options.py
21
Options.py
@@ -1,13 +1,15 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import abc
|
||||
import logging
|
||||
from copy import deepcopy
|
||||
import math
|
||||
import numbers
|
||||
import typing
|
||||
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
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
@@ -769,7 +771,7 @@ class VerifyKeys(metaclass=FreezeValidKeys):
|
||||
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] = {}
|
||||
supports_weighting = False
|
||||
|
||||
@@ -787,8 +789,14 @@ class OptionDict(Option[typing.Dict[str, typing.Any]], VerifyKeys):
|
||||
def get_option_name(self, value):
|
||||
return ", ".join(f"{key}: {v}" for key, v in value.items())
|
||||
|
||||
def __contains__(self, item):
|
||||
return item in self.value
|
||||
def __getitem__(self, item: str) -> typing.Any:
|
||||
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):
|
||||
@@ -949,6 +957,7 @@ class DeathLink(Toggle):
|
||||
|
||||
class ItemLinks(OptionList):
|
||||
"""Share part of your item pool with other players."""
|
||||
display_name = "Item Links"
|
||||
default = []
|
||||
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_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
|
||||
|
||||
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.sent_release = False
|
||||
self.sent_collect = False
|
||||
self.auto_hints = set()
|
||||
|
||||
async def server_auth(self, password_requested: bool = False):
|
||||
if password_requested and not self.password:
|
||||
@@ -153,6 +157,33 @@ async def parse_locations(data: List, ctx: GBContext):
|
||||
locations.append(loc_id)
|
||||
elif flags[flag_type][location_bytes_bits[loc_id]['byte']] & 1 << location_bytes_bits[loc_id]['bit']:
|
||||
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:
|
||||
await ctx.send_msgs([
|
||||
{"cmd": "StatusUpdate",
|
||||
|
||||
@@ -49,6 +49,8 @@ Currently, the following games are supported:
|
||||
* Bumper Stickers
|
||||
* Mega Man Battle Network 3: Blue Version
|
||||
* Muse Dash
|
||||
* DOOM 1993
|
||||
* Terraria
|
||||
|
||||
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
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from __future__ import annotations
|
||||
import os
|
||||
import sys
|
||||
import asyncio
|
||||
import typing
|
||||
import bsdiff4
|
||||
@@ -11,7 +12,7 @@ from NetUtils import NetworkItem, ClientStatus
|
||||
from worlds import undertale
|
||||
from MultiServer import mark_raw
|
||||
from CommonClient import CommonContext, server_loop, \
|
||||
gui_enabled, ClientCommandProcessor, get_base_parser
|
||||
gui_enabled, ClientCommandProcessor, logger, get_base_parser
|
||||
from Utils import async_start
|
||||
|
||||
|
||||
@@ -32,6 +33,12 @@ class UndertaleCommandProcessor(ClientCommandProcessor):
|
||||
self.ctx.patch_game()
|
||||
self.output("Patched.")
|
||||
|
||||
def _cmd_savepath(self, directory: str):
|
||||
"""Redirect to proper save data folder. (Use before connecting!)"""
|
||||
if isinstance(self.ctx, UndertaleContext):
|
||||
UndertaleContext.save_game_folder = directory
|
||||
self.output("Changed to the following directory: " + directory)
|
||||
|
||||
@mark_raw
|
||||
def _cmd_auto_patch(self, steaminstall: typing.Optional[str] = None):
|
||||
"""Patch the game automatically."""
|
||||
@@ -92,6 +99,7 @@ class UndertaleContext(CommonContext):
|
||||
def __init__(self, server_address, password):
|
||||
super().__init__(server_address, password)
|
||||
self.pieces_needed = 0
|
||||
self.finished_game = False
|
||||
self.game = "Undertale"
|
||||
self.got_deathlink = False
|
||||
self.syncing = False
|
||||
@@ -99,6 +107,8 @@ class UndertaleContext(CommonContext):
|
||||
self.tem_armor = False
|
||||
self.completed_count = 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):
|
||||
with open(os.getcwd() + "/Undertale/data.win", "rb") as f:
|
||||
@@ -227,7 +237,7 @@ async def process_undertale_cmd(ctx: UndertaleContext, cmd: str, args: dict):
|
||||
f.close()
|
||||
filename = f"check.spot"
|
||||
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.close()
|
||||
elif cmd == "LocationInfo":
|
||||
@@ -353,14 +363,14 @@ async def process_undertale_cmd(ctx: UndertaleContext, cmd: str, args: dict):
|
||||
if "checked_locations" in args:
|
||||
filename = f"check.spot"
|
||||
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.close()
|
||||
|
||||
elif cmd == "Bounced":
|
||||
tags = args.get("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:
|
||||
filename = f"FRISK" + str(data["player"]) + ".playerspot"
|
||||
with open(os.path.join(ctx.save_game_folder, filename), "w") as f:
|
||||
@@ -406,34 +416,38 @@ async def game_watcher(ctx: UndertaleContext):
|
||||
ctx.syncing = False
|
||||
if ctx.got_deathlink:
|
||||
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()
|
||||
sending = []
|
||||
victory = False
|
||||
found_routes = 0
|
||||
for root, dirs, files in os.walk(path):
|
||||
for file in files:
|
||||
if "DontBeMad.mad" in file and "DeathLink" in ctx.tags:
|
||||
if "DontBeMad.mad" in file:
|
||||
os.remove(root+"/"+file)
|
||||
await ctx.send_death()
|
||||
if "DeathLink" in ctx.tags:
|
||||
await ctx.send_death()
|
||||
if "scout" == file:
|
||||
sending = []
|
||||
with open(root+"/"+file, "r") as f:
|
||||
lines = f.readlines()
|
||||
try:
|
||||
with open(root+"/"+file, "r") as f:
|
||||
lines = f.readlines()
|
||||
for l in lines:
|
||||
if ctx.server_locations.__contains__(int(l)+12000):
|
||||
sending = sending + [int(l)+12000]
|
||||
await ctx.send_msgs([{"cmd": "LocationScouts", "locations": sending,
|
||||
"create_as_hint": int(2)}])
|
||||
os.remove(root+"/"+file)
|
||||
sending = sending + [int(l.rstrip('\n'))+12000]
|
||||
finally:
|
||||
await ctx.send_msgs([{"cmd": "LocationScouts", "locations": sending,
|
||||
"create_as_hint": int(2)}])
|
||||
os.remove(root+"/"+file)
|
||||
if "check.spot" in file:
|
||||
sending = []
|
||||
with open(root+"/"+file, "r") as f:
|
||||
lines = f.readlines()
|
||||
try:
|
||||
with open(root+"/"+file, "r") as f:
|
||||
lines = f.readlines()
|
||||
for l in lines:
|
||||
sending = sending+[(int(l))+12000]
|
||||
message = [{"cmd": "LocationChecks", "locations": sending}]
|
||||
await ctx.send_msgs(message)
|
||||
sending = sending+[(int(l.rstrip('\n')))+12000]
|
||||
finally:
|
||||
await ctx.send_msgs([{"cmd": "LocationChecks", "locations": sending}])
|
||||
if "victory" in file and str(ctx.route) in file:
|
||||
victory = True
|
||||
if ".playerspot" in file and "Online" not in ctx.tags:
|
||||
|
||||
228
Utils.py
228
Utils.py
@@ -13,8 +13,10 @@ import io
|
||||
import collections
|
||||
import importlib
|
||||
import logging
|
||||
from typing import BinaryIO, Coroutine, Optional, Set, Dict, Any, Union
|
||||
|
||||
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
|
||||
|
||||
try:
|
||||
@@ -138,13 +140,16 @@ def user_path(*path: str) -> str:
|
||||
user_path.cached_path = local_path()
|
||||
else:
|
||||
user_path.cached_path = home_path()
|
||||
# populate home from local - TODO: upgrade feature
|
||||
if user_path.cached_path != local_path() and not os.path.exists(user_path("host.yaml")):
|
||||
import shutil
|
||||
for dn in ("Players", "data/sprites"):
|
||||
shutil.copytree(local_path(dn), user_path(dn), dirs_exist_ok=True)
|
||||
for fn in ("manifest.json", "host.yaml"):
|
||||
shutil.copy2(local_path(fn), user_path(fn))
|
||||
# populate home from local
|
||||
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
|
||||
for dn in ("Players", "data/sprites"):
|
||||
shutil.copytree(local_path(dn), user_path(dn), dirs_exist_ok=True)
|
||||
for fn in ("manifest.json",):
|
||||
shutil.copy2(local_path(fn), user_path(fn))
|
||||
|
||||
return os.path.join(user_path.cached_path, *path)
|
||||
|
||||
@@ -238,155 +243,15 @@ def get_public_ipv6() -> str:
|
||||
return ip
|
||||
|
||||
|
||||
OptionsType = typing.Dict[str, typing.Dict[str, typing.Any]]
|
||||
OptionsType = Settings # TODO: remove ~2 versions after 0.4.1
|
||||
|
||||
|
||||
@cache_argsless
|
||||
def get_default_options() -> OptionsType:
|
||||
# Refer to host.yaml for comments as to what all these options mean.
|
||||
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 get_default_options() -> Settings: # TODO: remove ~2 versions after 0.4.1
|
||||
return Settings(None)
|
||||
|
||||
|
||||
def update_options(src: dict, dest: dict, filename: str, keys: list) -> OptionsType:
|
||||
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.")
|
||||
get_options = get_settings # TODO: add a warning ~2 versions after 0.4.1 and remove once all games are ported
|
||||
|
||||
|
||||
def persistent_store(category: str, key: typing.Any, value: typing.Any):
|
||||
@@ -454,12 +319,27 @@ def store_data_package_for_checksum(game: str, data: typing.Dict[str, Any]) -> N
|
||||
except Exception as 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
|
||||
|
||||
|
||||
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
|
||||
def get_unique_identifier():
|
||||
uuid = persistent_load().get("client", {}).get("uuid", None)
|
||||
@@ -677,7 +557,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]:
|
||||
def run(*args: str):
|
||||
return subprocess.run(args, capture_output=True, text=True).stdout.split("\n", 1)[0] or None
|
||||
@@ -688,11 +568,12 @@ def open_filename(title: str, filetypes: typing.Sequence[typing.Tuple[str, typin
|
||||
kdialog = which("kdialog")
|
||||
if kdialog:
|
||||
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")
|
||||
if zenity:
|
||||
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
|
||||
try:
|
||||
@@ -705,7 +586,38 @@ def open_filename(title: str, filetypes: typing.Sequence[typing.Tuple[str, typin
|
||||
else:
|
||||
root = tkinter.Tk()
|
||||
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 = None#which("kdialog")
|
||||
if kdialog:
|
||||
return run(kdialog, f"--title={title}", "--getexistingdirectory", suggest or ".")
|
||||
zenity = None#which("zenity")
|
||||
if zenity:
|
||||
z_filters = ("--directory",)
|
||||
selection = (f'--filename="{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:
|
||||
root = tkinter.Tk()
|
||||
root.withdraw()
|
||||
return tkinter.filedialog.askdirectory(title=title, mustexist=True, initialdir=suggest or None)
|
||||
|
||||
|
||||
def messagebox(title: str, text: str, error: bool = False) -> None:
|
||||
|
||||
27
WebHost.py
27
WebHost.py
@@ -10,6 +10,7 @@ ModuleUpdate.update()
|
||||
|
||||
# in case app gets imported by something like gunicorn
|
||||
import Utils
|
||||
import settings
|
||||
|
||||
Utils.local_path.cached_path = os.path.dirname(__file__) or "." # py3.8 is not abs. remove "." when dropping 3.8
|
||||
|
||||
@@ -18,9 +19,10 @@ 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
|
||||
import worlds
|
||||
|
||||
settings.no_gui = True
|
||||
configpath = os.path.abspath("config.yaml")
|
||||
if not os.path.exists(configpath): # fall back to config.yaml in home
|
||||
configpath = os.path.abspath(Utils.user_path('config.yaml'))
|
||||
@@ -40,6 +42,13 @@ def get_app():
|
||||
|
||||
db.bind(**app.config["PONY"])
|
||||
db.generate_mapping(create_tables=True)
|
||||
|
||||
for world in worlds.AutoWorldRegister.world_types.values():
|
||||
try:
|
||||
world.web.run_webhost_app_setup(app)
|
||||
except Exception as e:
|
||||
logging.exception(e)
|
||||
|
||||
return app
|
||||
|
||||
|
||||
@@ -72,6 +81,7 @@ def create_ordered_tutorials_file() -> typing.List[typing.Dict[str, typing.Any]]
|
||||
with zipfile.ZipFile(zipfile_path) as zf:
|
||||
for zfile in zf.infolist():
|
||||
if not zfile.is_dir() and "/docs/" in zfile.filename:
|
||||
zfile.filename = os.path.basename(zfile.filename)
|
||||
zf.extract(zfile, target_path)
|
||||
else:
|
||||
source_path = Utils.local_path(os.path.dirname(world.__file__), "docs")
|
||||
@@ -117,12 +127,17 @@ if __name__ == "__main__":
|
||||
multiprocessing.freeze_support()
|
||||
multiprocessing.set_start_method('spawn')
|
||||
logging.basicConfig(format='[%(asctime)s] %(message)s', level=logging.INFO)
|
||||
try:
|
||||
update_sprites_lttp()
|
||||
except Exception as e:
|
||||
logging.exception(e)
|
||||
logging.warning("Could not update LttP sprites.")
|
||||
|
||||
for world in worlds.AutoWorldRegister.world_types.values():
|
||||
try:
|
||||
world.web.run_webhost_setup()
|
||||
except Exception as e:
|
||||
logging.exception(e)
|
||||
|
||||
app = get_app()
|
||||
|
||||
del world, worlds
|
||||
|
||||
create_options_files()
|
||||
create_ordered_tutorials_file()
|
||||
if app.config["SELFLAUNCH"]:
|
||||
|
||||
@@ -130,6 +130,7 @@ def gen_game(gen_options: dict, meta: Optional[Dict[str, Any]] = None, owner=Non
|
||||
erargs.teams = 1
|
||||
erargs.plando_options = PlandoOptions.from_set(meta.setdefault("plando_options",
|
||||
{"bosses", "items", "connections", "texts"}))
|
||||
erargs.skip_prog_balancing = False
|
||||
|
||||
name_counter = Counter()
|
||||
for player, (playerfile, settings) in enumerate(gen_options.items(), 1):
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
import os
|
||||
import threading
|
||||
import json
|
||||
|
||||
from Utils import local_path, user_path
|
||||
from worlds.alttp.Rom import Sprite
|
||||
|
||||
|
||||
def update_sprites_lttp():
|
||||
from tkinter import Tk
|
||||
from LttPAdjuster import get_image_for_sprite
|
||||
from LttPAdjuster import BackgroundTaskProgress
|
||||
from LttPAdjuster import BackgroundTaskProgressNullWindow
|
||||
from LttPAdjuster import update_sprites
|
||||
|
||||
# Target directories
|
||||
input_dir = user_path("data", "sprites", "alttpr")
|
||||
output_dir = local_path("WebHostLib", "static", "generated") # TODO: move to user_path
|
||||
|
||||
os.makedirs(os.path.join(output_dir, "sprites"), exist_ok=True)
|
||||
# update sprites through gui.py's functions
|
||||
done = threading.Event()
|
||||
try:
|
||||
top = Tk()
|
||||
except:
|
||||
task = BackgroundTaskProgressNullWindow(update_sprites, lambda successful, resultmessage: done.set())
|
||||
else:
|
||||
top.withdraw()
|
||||
task = BackgroundTaskProgress(top, update_sprites, "Updating Sprites", lambda succesful, resultmessage: done.set())
|
||||
while not done.isSet():
|
||||
task.do_events()
|
||||
|
||||
spriteData = []
|
||||
|
||||
for file in (file for file in os.listdir(input_dir) if not file.startswith(".")):
|
||||
sprite = Sprite(os.path.join(input_dir, file))
|
||||
|
||||
if not sprite.name:
|
||||
print("Warning:", file, "has no name.")
|
||||
sprite.name = file.split(".", 1)[0]
|
||||
if sprite.valid:
|
||||
with open(os.path.join(output_dir, "sprites", f"{os.path.splitext(file)[0]}.gif"), 'wb') as image:
|
||||
image.write(get_image_for_sprite(sprite, True))
|
||||
spriteData.append({"file": file, "author": sprite.author_name, "name": sprite.name})
|
||||
else:
|
||||
print(file, "dropped, as it has no valid sprite data.")
|
||||
spriteData.sort(key=lambda entry: entry["name"])
|
||||
with open(f'{output_dir}/spriteData.json', 'w') as file:
|
||||
json.dump({"sprites": spriteData}, file, indent=1)
|
||||
return spriteData
|
||||
@@ -5,7 +5,8 @@ html{
|
||||
}
|
||||
|
||||
#player-settings{
|
||||
max-width: 1000px;
|
||||
box-sizing: border-box;
|
||||
max-width: 1024px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
background-color: rgba(0, 0, 0, 0.15);
|
||||
@@ -163,6 +164,11 @@ html{
|
||||
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{
|
||||
display: block;
|
||||
min-width: 200px;
|
||||
@@ -177,18 +183,31 @@ html{
|
||||
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{
|
||||
justify-content: flex-start;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
#player-settings .left, #player-settings .right{
|
||||
flex-grow: unset;
|
||||
#player-settings .left,
|
||||
#player-settings .right {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#game-options table {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
#game-options table label{
|
||||
display: block;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
#game-options table tr td {
|
||||
width: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
28
WebHostLib/templates/hintTable.html
Normal file
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 %}
|
||||
<footer id="island-footer">
|
||||
<div id="copyright-notice">Copyright 2022 Archipelago</div>
|
||||
<div id="copyright-notice">Copyright 2023 Archipelago</div>
|
||||
<div id="links">
|
||||
<a href="/sitemap">Site Map</a>
|
||||
-
|
||||
|
||||
@@ -128,20 +128,30 @@
|
||||
tracked_team=team, tracked_player=player)}}">{{ loop.index }}</a></td>
|
||||
<td>{{ player_names[(team, loop.index)]|e }}</td>
|
||||
{%- for area in ordered_areas -%}
|
||||
{%- set checks_done = checks[area] -%}
|
||||
{%- set checks_total = checks_in_area[player][area] -%}
|
||||
{%- if checks_done == checks_total -%}
|
||||
<td class="item-acquired center-column">
|
||||
{{ checks_done }}/{{ checks_total }}</td>
|
||||
{%- else -%}
|
||||
<td class="center-column">{{ checks_done }}/{{ checks_total }}</td>
|
||||
{%- endif -%}
|
||||
{%- if area in key_locations -%}
|
||||
<td class="center-column">{{ inventory[team][player][small_key_ids[area]] }}</td>
|
||||
{%- endif -%}
|
||||
{%- if area in big_key_locations -%}
|
||||
<td class="center-column">{% if inventory[team][player][big_key_ids[area]] %}✔️{% endif %}</td>
|
||||
{%- endif -%}
|
||||
{% if player in checks_in_area and area in checks_in_area[player] %}
|
||||
{%- set checks_done = checks[area] -%}
|
||||
{%- set checks_total = checks_in_area[player][area] -%}
|
||||
{%- if checks_done == checks_total -%}
|
||||
<td class="item-acquired center-column">
|
||||
{{ checks_done }}/{{ checks_total }}</td>
|
||||
{%- else -%}
|
||||
<td class="center-column">{{ checks_done }}/{{ checks_total }}</td>
|
||||
{%- endif -%}
|
||||
{%- if area in key_locations -%}
|
||||
<td class="center-column">{{ inventory[team][player][small_key_ids[area]] }}</td>
|
||||
{%- endif -%}
|
||||
{%- if area in big_key_locations -%}
|
||||
<td class="center-column">{% if inventory[team][player][big_key_ids[area]] %}✔️{% endif %}</td>
|
||||
{%- endif -%}
|
||||
{% else %}
|
||||
<td class="center-column"></td>
|
||||
{%- if area in key_locations -%}
|
||||
<td class="center-column"></td>
|
||||
{%- endif -%}
|
||||
{%- if area in big_key_locations -%}
|
||||
<td class="center-column"></td>
|
||||
{%- endif -%}
|
||||
{% endif %}
|
||||
{%- endfor -%}
|
||||
<td class="center-column">{{ percent_total_checks_done[team][player] }}</td>
|
||||
{%- if activity_timers[(team, player)] -%}
|
||||
@@ -155,34 +165,7 @@
|
||||
</table>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% 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 %}
|
||||
{% include "hintTable.html" with context %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
{# implement this block in game-specific multi trackers #}
|
||||
{% endblock %}
|
||||
<td class="center-column" data-sort="{{ checks["Total"] }}">
|
||||
{{ checks["Total"] }}/{{ checks_in_area[player]["Total"] }}
|
||||
{{ checks["Total"] }}/{{ locations[player] | length }}
|
||||
</td>
|
||||
<td class="center-column">{{ percent_total_checks_done[team][player] }}</td>
|
||||
{%- if activity_timers[team, player] -%}
|
||||
@@ -67,34 +67,7 @@
|
||||
</table>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% 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 %}
|
||||
{% include "hintTable.html" with context %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{%- if enabled_multiworld_trackers|length > 1 -%}
|
||||
<div id="tracker-navigation">
|
||||
{% for enabled_tracker in enabled_multiworld_trackers %}
|
||||
{% set tracker_url = url_for(enabled_tracker.endpoint, tracker=room.tracker) %}
|
||||
{% set tracker_url = url_for(enabled_tracker.endpoint, tracker=room.tracker, game=enabled_tracker.name) %}
|
||||
<a class="tracker-navigation-button{% if enabled_tracker.current %} selected{% endif %}"
|
||||
href="{{ tracker_url }}">{{ enabled_tracker.name }}</a>
|
||||
{% endfor %}
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
import collections
|
||||
import datetime
|
||||
import typing
|
||||
from typing import Counter, Optional, Dict, Any, Tuple
|
||||
import pkgutil
|
||||
from typing import Counter, Optional, Dict, Any, Tuple, List
|
||||
from uuid import UUID
|
||||
|
||||
from flask import render_template
|
||||
from jinja2 import pass_context, runtime
|
||||
from jinja2 import pass_context, runtime, Template
|
||||
from werkzeug.exceptions import abort
|
||||
|
||||
from MultiServer import Context, get_saving_second
|
||||
from NetUtils import SlotType
|
||||
from NetUtils import SlotType, NetworkSlot
|
||||
from Utils import restricted_loads
|
||||
from worlds import lookup_any_item_id_to_name, lookup_any_location_id_to_name, network_data_package
|
||||
from worlds import lookup_any_item_id_to_name, lookup_any_location_id_to_name, network_data_package, AutoWorldRegister
|
||||
from worlds.alttp import Items
|
||||
from . import app, cache
|
||||
from .models import GameDataPackage, Room
|
||||
@@ -264,16 +265,17 @@ def get_static_room_data(room: Room):
|
||||
multidata = Context.decompress(room.seed.multidata)
|
||||
# in > 100 players this can take a bit of time and is the main reason for the cache
|
||||
locations: Dict[int, Dict[int, Tuple[int, int, int]]] = multidata['locations']
|
||||
names: Dict[int, Dict[int, str]] = multidata["names"]
|
||||
games = {}
|
||||
names: List[List[str]] = multidata.get("names", [])
|
||||
games = multidata.get("games", {})
|
||||
groups = {}
|
||||
custom_locations = {}
|
||||
custom_items = {}
|
||||
if "slot_info" in multidata:
|
||||
games = {slot: slot_info.game for slot, slot_info in multidata["slot_info"].items()}
|
||||
groups = {slot: slot_info.group_members for slot, slot_info in multidata["slot_info"].items()
|
||||
slot_info_dict: Dict[int, NetworkSlot] = multidata["slot_info"]
|
||||
games = {slot: slot_info.game for slot, slot_info in slot_info_dict.items()}
|
||||
groups = {slot: slot_info.group_members for slot, slot_info in slot_info_dict.items()
|
||||
if slot_info.type == SlotType.group}
|
||||
|
||||
names = [[slot_info.name for slot, slot_info in sorted(slot_info_dict.items())]]
|
||||
for game in games.values():
|
||||
if game not in multidata["datapackage"]:
|
||||
continue
|
||||
@@ -290,8 +292,7 @@ def get_static_room_data(room: Room):
|
||||
{id_: name for name, id_ in game_data["location_name_to_id"].items()})
|
||||
custom_items.update(
|
||||
{id_: name for name, id_ in game_data["item_name_to_id"].items()})
|
||||
elif "games" in multidata:
|
||||
games = multidata["games"]
|
||||
|
||||
seed_checks_in_area = checks_in_area.copy()
|
||||
|
||||
use_door_tracker = False
|
||||
@@ -302,14 +303,17 @@ def get_static_room_data(room: Room):
|
||||
seed_checks_in_area[area] += len(checks)
|
||||
seed_checks_in_area["Total"] = 249
|
||||
|
||||
player_checks_in_area = {playernumber: {areaname: len(multidata["checks_in_area"][playernumber][areaname])
|
||||
if areaname != "Total" else multidata["checks_in_area"][playernumber]["Total"]
|
||||
for areaname in ordered_areas}
|
||||
for playernumber in range(1, len(names[0]) + 1)
|
||||
if playernumber not in groups}
|
||||
player_checks_in_area = {
|
||||
playernumber: {
|
||||
areaname: len(multidata["checks_in_area"][playernumber][areaname]) if areaname != "Total" else
|
||||
multidata["checks_in_area"][playernumber]["Total"]
|
||||
for areaname in ordered_areas
|
||||
}
|
||||
for playernumber in multidata["checks_in_area"]
|
||||
}
|
||||
|
||||
player_location_to_area = {playernumber: get_location_table(multidata["checks_in_area"][playernumber])
|
||||
for playernumber in range(1, len(names[0]) + 1)
|
||||
if playernumber not in groups}
|
||||
for playernumber in multidata["checks_in_area"]}
|
||||
saving_second = get_saving_second(multidata["seed_name"])
|
||||
result = locations, names, use_door_tracker, player_checks_in_area, player_location_to_area, \
|
||||
multidata["precollected_items"], games, multidata["slot_data"], groups, saving_second, \
|
||||
@@ -343,7 +347,7 @@ def _get_player_tracker(tracker: UUID, tracked_team: int, tracked_player: int, w
|
||||
precollected_items, games, slot_data, groups, saving_second, custom_locations, custom_items = \
|
||||
get_static_room_data(room)
|
||||
player_name = names[tracked_team][tracked_player - 1]
|
||||
location_to_area = player_location_to_area[tracked_player]
|
||||
location_to_area = player_location_to_area.get(tracked_player, {})
|
||||
inventory = collections.Counter()
|
||||
checks_done = {loc_name: 0 for loc_name in default_locations}
|
||||
|
||||
@@ -375,15 +379,18 @@ def _get_player_tracker(tracker: UUID, tracked_team: int, tracked_player: int, w
|
||||
if recipient in slots_aimed_at_player: # a check done for the tracked player
|
||||
attribute_item_solo(inventory, item)
|
||||
if ms_player == tracked_player: # a check done by the tracked player
|
||||
checks_done[location_to_area[location]] += 1
|
||||
area_name = location_to_area.get(location, None)
|
||||
if area_name:
|
||||
checks_done[area_name] += 1
|
||||
checks_done["Total"] += 1
|
||||
specific_tracker = game_specific_trackers.get(games[tracked_player], None)
|
||||
if specific_tracker and not want_generic:
|
||||
tracker = specific_tracker(multisave, room, locations, inventory, tracked_team, tracked_player, player_name,
|
||||
seed_checks_in_area, checks_done, slot_data[tracked_player], saving_second)
|
||||
seed_checks_in_area, checks_done, slot_data[tracked_player], saving_second)
|
||||
else:
|
||||
tracker = __renderGenericTracker(multisave, room, locations, inventory, tracked_team, tracked_player, player_name,
|
||||
seed_checks_in_area, checks_done, saving_second, custom_locations, custom_items)
|
||||
tracker = __renderGenericTracker(multisave, room, locations, inventory, tracked_team, tracked_player,
|
||||
player_name, seed_checks_in_area, checks_done, saving_second,
|
||||
custom_locations, custom_items)
|
||||
|
||||
return (saving_second - datetime.datetime.now().second) % 60 or 60, tracker
|
||||
|
||||
@@ -1325,87 +1332,6 @@ def __renderGenericTracker(multisave: Dict[str, Any], room: Room, locations: Dic
|
||||
custom_items=custom_items, custom_locations=custom_locations)
|
||||
|
||||
|
||||
def get_enabled_multiworld_trackers(room: Room, current: str):
|
||||
enabled = [
|
||||
{
|
||||
"name": "Generic",
|
||||
"endpoint": "get_multiworld_tracker",
|
||||
"current": current == "Generic"
|
||||
}
|
||||
]
|
||||
for game_name, endpoint in multi_trackers.items():
|
||||
if any(slot.game == game_name for slot in room.seed.slots) or current == game_name:
|
||||
enabled.append({
|
||||
"name": game_name,
|
||||
"endpoint": endpoint.__name__,
|
||||
"current": current == game_name}
|
||||
)
|
||||
return enabled
|
||||
|
||||
|
||||
def _get_multiworld_tracker_data(tracker: UUID) -> typing.Optional[typing.Dict[str, typing.Any]]:
|
||||
room: Room = Room.get(tracker=tracker)
|
||||
if not room:
|
||||
return None
|
||||
|
||||
locations, names, use_door_tracker, checks_in_area, player_location_to_area, \
|
||||
precollected_items, games, slot_data, groups, saving_second, custom_locations, custom_items = \
|
||||
get_static_room_data(room)
|
||||
|
||||
checks_done = {teamnumber: {playernumber: {loc_name: 0 for loc_name in default_locations}
|
||||
for playernumber in range(1, len(team) + 1) if playernumber not in groups}
|
||||
for teamnumber, team in enumerate(names)}
|
||||
|
||||
percent_total_checks_done = {teamnumber: {playernumber: 0
|
||||
for playernumber in range(1, len(team) + 1) if playernumber not in groups}
|
||||
for teamnumber, team in enumerate(names)}
|
||||
|
||||
hints = {team: set() for team in range(len(names))}
|
||||
if room.multisave:
|
||||
multisave = restricted_loads(room.multisave)
|
||||
else:
|
||||
multisave = {}
|
||||
if "hints" in multisave:
|
||||
for (team, slot), slot_hints in multisave["hints"].items():
|
||||
hints[team] |= set(slot_hints)
|
||||
|
||||
for (team, player), locations_checked in multisave.get("location_checks", {}).items():
|
||||
if player in groups:
|
||||
continue
|
||||
player_locations = locations[player]
|
||||
checks_done[team][player]["Total"] = sum(1 for loc in locations_checked if loc in player_locations)
|
||||
percent_total_checks_done[team][player] = int(checks_done[team][player]["Total"] /
|
||||
checks_in_area[player]["Total"] * 100) \
|
||||
if checks_in_area[player]["Total"] else 100
|
||||
|
||||
activity_timers = {}
|
||||
now = datetime.datetime.utcnow()
|
||||
for (team, player), timestamp in multisave.get("client_activity_timers", []):
|
||||
activity_timers[team, player] = now - datetime.datetime.utcfromtimestamp(timestamp)
|
||||
|
||||
player_names = {}
|
||||
states: typing.Dict[typing.Tuple[int, int], int] = {}
|
||||
for team, names in enumerate(names):
|
||||
for player, name in enumerate(names, 1):
|
||||
player_names[team, player] = name
|
||||
states[team, player] = multisave.get("client_game_state", {}).get((team, player), 0)
|
||||
long_player_names = player_names.copy()
|
||||
for (team, player), alias in multisave.get("name_aliases", {}).items():
|
||||
player_names[team, player] = alias
|
||||
long_player_names[(team, player)] = f"{alias} ({long_player_names[team, player]})"
|
||||
|
||||
video = {}
|
||||
for (team, player), data in multisave.get("video", []):
|
||||
video[team, player] = data
|
||||
|
||||
return dict(player_names=player_names, room=room, checks_done=checks_done,
|
||||
percent_total_checks_done=percent_total_checks_done, checks_in_area=checks_in_area,
|
||||
activity_timers=activity_timers, video=video, hints=hints,
|
||||
long_player_names=long_player_names,
|
||||
multisave=multisave, precollected_items=precollected_items, groups=groups,
|
||||
locations=locations, games=games, states=states)
|
||||
|
||||
|
||||
def _get_inventory_data(data: typing.Dict[str, typing.Any]) -> typing.Dict[int, typing.Dict[int, int]]:
|
||||
inventory = {teamnumber: {playernumber: collections.Counter() for playernumber in team_data}
|
||||
for teamnumber, team_data in data["checks_done"].items()}
|
||||
@@ -1426,32 +1352,6 @@ def _get_inventory_data(data: typing.Dict[str, typing.Any]) -> typing.Dict[int,
|
||||
inventory[team][recipient][item_id] += 1
|
||||
return inventory
|
||||
|
||||
|
||||
@app.route('/tracker/<suuid:tracker>')
|
||||
@cache.memoize(timeout=60) # multisave is currently created at most every minute
|
||||
def get_multiworld_tracker(tracker: UUID):
|
||||
data = _get_multiworld_tracker_data(tracker)
|
||||
if not data:
|
||||
abort(404)
|
||||
|
||||
data["enabled_multiworld_trackers"] = get_enabled_multiworld_trackers(data["room"], "Generic")
|
||||
|
||||
return render_template("multiTracker.html", **data)
|
||||
|
||||
|
||||
@app.route('/tracker/<suuid:tracker>/Factorio')
|
||||
@cache.memoize(timeout=60) # multisave is currently created at most every minute
|
||||
def get_Factorio_multiworld_tracker(tracker: UUID):
|
||||
data = _get_multiworld_tracker_data(tracker)
|
||||
if not data:
|
||||
abort(404)
|
||||
|
||||
data["inventory"] = _get_inventory_data(data)
|
||||
data["enabled_multiworld_trackers"] = get_enabled_multiworld_trackers(data["room"], "Factorio")
|
||||
|
||||
return render_template("multiFactorioTracker.html", **data)
|
||||
|
||||
|
||||
@app.route('/tracker/<suuid:tracker>/A Link to the Past')
|
||||
@cache.memoize(timeout=60) # multisave is currently created at most every minute
|
||||
def get_LttP_multiworld_tracker(tracker: UUID):
|
||||
@@ -1509,8 +1409,8 @@ def get_LttP_multiworld_tracker(tracker: UUID):
|
||||
checks_done[team][player][player_location_to_area[player][location]] += 1
|
||||
checks_done[team][player]["Total"] += 1
|
||||
percent_total_checks_done[team][player] = int(
|
||||
checks_done[team][player]["Total"] / seed_checks_in_area[player]["Total"] * 100) if \
|
||||
seed_checks_in_area[player]["Total"] else 100
|
||||
checks_done[team][player]["Total"] / len(player_locations) * 100) if \
|
||||
player_locations else 100
|
||||
|
||||
for (team, player), game_state in multisave.get("client_game_state", {}).items():
|
||||
if player in groups:
|
||||
@@ -1577,7 +1477,142 @@ game_specific_trackers: typing.Dict[str, typing.Callable] = {
|
||||
"Starcraft 2 Wings of Liberty": __renderSC2WoLTracker
|
||||
}
|
||||
|
||||
# MultiTrackers
|
||||
|
||||
@app.route('/tracker/<suuid:tracker>')
|
||||
@cache.memoize(timeout=60) # multisave is currently created at most every minute
|
||||
def get_multiworld_tracker(tracker: UUID) -> str:
|
||||
data = _get_multiworld_tracker_data(tracker)
|
||||
if not data:
|
||||
abort(404)
|
||||
|
||||
data["enabled_multiworld_trackers"] = get_enabled_multiworld_trackers(data["room"], "Generic")
|
||||
|
||||
return render_template("multiTracker.html", **data)
|
||||
|
||||
def get_enabled_multiworld_trackers(room: Room, current: str) -> typing.List[typing.Dict[str, typing.Any]]:
|
||||
enabled = [
|
||||
{
|
||||
"name": "Generic",
|
||||
"endpoint": "get_multiworld_tracker",
|
||||
"current": current == "Generic"
|
||||
}
|
||||
]
|
||||
for game_name, endpoint in multi_trackers.items():
|
||||
if any(slot.game == game_name for slot in room.seed.slots) or current == game_name:
|
||||
enabled.append({
|
||||
"name": game_name,
|
||||
"endpoint": endpoint.__name__,
|
||||
"current": current == game_name}
|
||||
)
|
||||
return enabled
|
||||
|
||||
|
||||
def _get_multiworld_tracker_data(tracker: UUID) -> typing.Optional[typing.Dict[str, typing.Any]]:
|
||||
room: Room = Room.get(tracker=tracker)
|
||||
if not room:
|
||||
return None
|
||||
|
||||
locations, names, use_door_tracker, checks_in_area, player_location_to_area, \
|
||||
precollected_items, games, slot_data, groups, saving_second, custom_locations, custom_items = \
|
||||
get_static_room_data(room)
|
||||
|
||||
checks_done = {teamnumber: {playernumber: {loc_name: 0 for loc_name in default_locations}
|
||||
for playernumber in range(1, len(team) + 1) if playernumber not in groups}
|
||||
for teamnumber, team in enumerate(names)}
|
||||
|
||||
percent_total_checks_done = {teamnumber: {playernumber: 0
|
||||
for playernumber in range(1, len(team) + 1) if playernumber not in groups}
|
||||
for teamnumber, team in enumerate(names)}
|
||||
|
||||
hints = {team: set() for team in range(len(names))}
|
||||
if room.multisave:
|
||||
multisave = restricted_loads(room.multisave)
|
||||
else:
|
||||
multisave = {}
|
||||
if "hints" in multisave:
|
||||
for (team, slot), slot_hints in multisave["hints"].items():
|
||||
hints[team] |= set(slot_hints)
|
||||
|
||||
for (team, player), locations_checked in multisave.get("location_checks", {}).items():
|
||||
if player in groups:
|
||||
continue
|
||||
player_locations = locations[player]
|
||||
checks_done[team][player]["Total"] = len(locations_checked)
|
||||
percent_total_checks_done[team][player] = int(checks_done[team][player]["Total"] /
|
||||
len(player_locations) * 100) \
|
||||
if player_locations else 100
|
||||
|
||||
activity_timers = {}
|
||||
now = datetime.datetime.utcnow()
|
||||
for (team, player), timestamp in multisave.get("client_activity_timers", []):
|
||||
activity_timers[team, player] = now - datetime.datetime.utcfromtimestamp(timestamp)
|
||||
|
||||
player_names = {}
|
||||
states: typing.Dict[typing.Tuple[int, int], int] = {}
|
||||
for team, names in enumerate(names):
|
||||
for player, name in enumerate(names, 1):
|
||||
player_names[team, player] = name
|
||||
states[team, player] = multisave.get("client_game_state", {}).get((team, player), 0)
|
||||
long_player_names = player_names.copy()
|
||||
for (team, player), alias in multisave.get("name_aliases", {}).items():
|
||||
player_names[team, player] = alias
|
||||
long_player_names[(team, player)] = f"{alias} ({long_player_names[team, player]})"
|
||||
|
||||
video = {}
|
||||
for (team, player), data in multisave.get("video", []):
|
||||
video[team, player] = data
|
||||
|
||||
return dict(
|
||||
player_names=player_names, room=room, checks_done=checks_done,
|
||||
percent_total_checks_done=percent_total_checks_done, checks_in_area=checks_in_area,
|
||||
activity_timers=activity_timers, video=video, hints=hints,
|
||||
long_player_names=long_player_names,
|
||||
multisave=multisave, precollected_items=precollected_items, groups=groups,
|
||||
locations=locations, games=games, states=states,
|
||||
custom_locations=custom_locations, custom_items=custom_items,
|
||||
)
|
||||
|
||||
multi_trackers: typing.Dict[str, typing.Callable] = {
|
||||
"A Link to the Past": get_LttP_multiworld_tracker,
|
||||
"Factorio": get_Factorio_multiworld_tracker,
|
||||
}
|
||||
|
||||
class MultiTrackerData(typing.NamedTuple):
|
||||
template: Template
|
||||
item_name_to_id: typing.Dict[str, int]
|
||||
location_name_to_id: typing.Dict[str, int]
|
||||
|
||||
multi_tracker_data: typing.Dict[str, MultiTrackerData] = {}
|
||||
|
||||
@app.route("/tracker/<suuid:tracker>/<game>")
|
||||
@cache.memoize(timeout=60) # multisave is currently created up to every minute
|
||||
def get_game_multiworld_tracker(tracker: UUID, game: str) -> str:
|
||||
current_multi_tracker_data = multi_tracker_data.get(game, None)
|
||||
if not current_multi_tracker_data:
|
||||
abort(404)
|
||||
data = _get_multiworld_tracker_data(tracker)
|
||||
if not data:
|
||||
abort(404)
|
||||
|
||||
data["inventory"] = _get_inventory_data(data)
|
||||
data["enabled_multiworld_trackers"] = get_enabled_multiworld_trackers(data["room"], game)
|
||||
data["item_name_to_id"] = current_multi_tracker_data.item_name_to_id
|
||||
data["location_name_to_id"] = current_multi_tracker_data.location_name_to_id
|
||||
|
||||
return render_template(current_multi_tracker_data.template, **data)
|
||||
|
||||
def register_multitrackers() -> None:
|
||||
for world in AutoWorldRegister.world_types.values():
|
||||
multitracker = world.web.multitracker_template
|
||||
if multitracker:
|
||||
multitracker_template = pkgutil.get_data(world.__module__, multitracker).decode()
|
||||
multitracker_template = app.jinja_env.from_string(multitracker_template)
|
||||
|
||||
multi_trackers[world.game] = get_game_multiworld_tracker
|
||||
multi_tracker_data[world.game] = MultiTrackerData(
|
||||
multitracker_template,
|
||||
world.item_name_to_id,
|
||||
world.location_name_to_id,
|
||||
)
|
||||
|
||||
register_multitrackers()
|
||||
|
||||
@@ -423,9 +423,9 @@ async def zillion_sync_task(ctx: ZillionContext) -> None:
|
||||
async_start(ctx.send_connect())
|
||||
log_no_spam("logging in to server...")
|
||||
await asyncio.wait((
|
||||
ctx.got_slot_data.wait(),
|
||||
ctx.exit_event.wait(),
|
||||
asyncio.sleep(6)
|
||||
asyncio.create_task(ctx.got_slot_data.wait()),
|
||||
asyncio.create_task(ctx.exit_event.wait()),
|
||||
asyncio.create_task(asyncio.sleep(6))
|
||||
), return_when=asyncio.FIRST_COMPLETED) # to not spam connect packets
|
||||
else: # not correct seed name
|
||||
log_no_spam("incorrect seed - did you mix up roms?")
|
||||
@@ -447,9 +447,9 @@ async def zillion_sync_task(ctx: ZillionContext) -> None:
|
||||
ctx.known_name = name
|
||||
async_start(ctx.connect())
|
||||
await asyncio.wait((
|
||||
ctx.got_room_info.wait(),
|
||||
ctx.exit_event.wait(),
|
||||
asyncio.sleep(6)
|
||||
asyncio.create_task(ctx.got_room_info.wait()),
|
||||
asyncio.create_task(ctx.exit_event.wait()),
|
||||
asyncio.create_task(asyncio.sleep(6))
|
||||
), return_when=asyncio.FIRST_COMPLETED)
|
||||
else: # no name found in game
|
||||
if not help_message_shown:
|
||||
|
||||
347
_speedups.pyx
Normal file
347
_speedups.pyx
Normal file
@@ -0,0 +1,347 @@
|
||||
#cython: language_level=3
|
||||
#distutils: language = c++
|
||||
|
||||
"""
|
||||
Provides faster implementation of some core parts.
|
||||
This is deliberately .pyx because using a non-compiled "pure python" may be slower.
|
||||
"""
|
||||
|
||||
# pip install cython cymem
|
||||
import cython
|
||||
import warnings
|
||||
from cpython cimport PyObject
|
||||
from typing import Any, Dict, Iterable, Iterator, Generator, Sequence, Tuple, TypeVar, Union, Set, List, TYPE_CHECKING
|
||||
from cymem.cymem cimport Pool
|
||||
from libc.stdint cimport int64_t, uint32_t
|
||||
from libcpp.set cimport set as std_set
|
||||
from collections import defaultdict
|
||||
|
||||
cdef extern from *:
|
||||
"""
|
||||
// avoid warning from cython-generated code with MSVC + pyximport
|
||||
#ifdef _MSC_VER
|
||||
#pragma warning( disable: 4551 )
|
||||
#endif
|
||||
"""
|
||||
|
||||
ctypedef uint32_t ap_player_t # on AMD64 this is faster (and smaller) than 64bit ints
|
||||
ctypedef uint32_t ap_flags_t
|
||||
ctypedef int64_t ap_id_t
|
||||
|
||||
cdef ap_player_t MAX_PLAYER_ID = 1000000 # limit the size of indexing array
|
||||
cdef size_t INVALID_SIZE = <size_t>(-1) # this is all 0xff... adding 1 results in 0, but it's not negative
|
||||
|
||||
|
||||
cdef struct LocationEntry:
|
||||
# layout is so that
|
||||
# 64bit player: location+sender and item+receiver 128bit comparisons, if supported
|
||||
# 32bit player: aligned to 32/64bit with no unused space
|
||||
ap_id_t location
|
||||
ap_player_t sender
|
||||
ap_player_t receiver
|
||||
ap_id_t item
|
||||
ap_flags_t flags
|
||||
|
||||
|
||||
cdef struct IndexEntry:
|
||||
size_t start
|
||||
size_t count
|
||||
|
||||
|
||||
cdef class LocationStore:
|
||||
"""Compact store for locations and their items in a MultiServer"""
|
||||
# The original implementation uses Dict[int, Dict[int, Tuple(int, int, int]]
|
||||
# with sender, location, (item, receiver, flags).
|
||||
# This implementation is a flat list of (sender, location, item, receiver, flags) using native integers
|
||||
# as well as some mapping arrays used to speed up stuff, saving a lot of memory while speeding up hints.
|
||||
# Using std::map might be worth investigating, but memory overhead would be ~100% compared to arrays.
|
||||
|
||||
cdef Pool _mem
|
||||
cdef object _len
|
||||
cdef LocationEntry* entries # 3.2MB/100k items
|
||||
cdef size_t entry_count
|
||||
cdef IndexEntry* sender_index # 16KB/1000 players
|
||||
cdef size_t sender_index_size
|
||||
cdef list _keys # ~36KB/1000 players, speed up iter (28 per int + 8 per list entry)
|
||||
cdef list _items # ~64KB/1000 players, speed up items (56 per tuple + 8 per list entry)
|
||||
cdef list _proxies # ~92KB/1000 players, speed up self[player] (56 per struct + 28 per len + 8 per list entry)
|
||||
cdef PyObject** _raw_proxies # 8K/1000 players, faster access to _proxies, but does not keep a ref
|
||||
|
||||
def get_size(self):
|
||||
from sys import getsizeof
|
||||
size = getsizeof(self) + getsizeof(self._mem) + getsizeof(self._len) \
|
||||
+ sizeof(LocationEntry) * self.entry_count + sizeof(IndexEntry) * self.sender_index_size
|
||||
size += getsizeof(self._keys) + getsizeof(self._items) + getsizeof(self._proxies)
|
||||
size += sum(sizeof(key) for key in self._keys)
|
||||
size += sum(sizeof(item) for item in self._items)
|
||||
size += sum(sizeof(proxy) for proxy in self._proxies)
|
||||
size += sizeof(self._raw_proxies[0]) * self.sender_index_size
|
||||
return size
|
||||
|
||||
def __cinit__(self, locations_dict: Dict[int, Dict[int, Sequence[int]]]) -> None:
|
||||
self._mem = None
|
||||
self._keys = None
|
||||
self._items = None
|
||||
self._proxies = None
|
||||
self._len = 0
|
||||
self.entries = NULL
|
||||
self.entry_count = 0
|
||||
self.sender_index = NULL
|
||||
self.sender_index_size = 0
|
||||
self._raw_proxies = NULL
|
||||
|
||||
def __init__(self, locations_dict: Dict[int, Dict[int, Sequence[int]]]) -> None:
|
||||
self._mem = Pool()
|
||||
cdef object key
|
||||
self._keys = []
|
||||
self._items = []
|
||||
self._proxies = []
|
||||
|
||||
# iterate over everything to get all maxima and validate everything
|
||||
cdef size_t max_sender = INVALID_SIZE # keep track of highest used player id for indexing
|
||||
cdef size_t sender_count = 0
|
||||
cdef size_t count = 0
|
||||
for sender, locations in locations_dict.items():
|
||||
# we don't require the dict to be sorted here
|
||||
if not isinstance(sender, int) or sender < 1 or sender > MAX_PLAYER_ID:
|
||||
raise ValueError(f"Invalid player id {sender} for location")
|
||||
if max_sender == INVALID_SIZE:
|
||||
max_sender = sender
|
||||
else:
|
||||
max_sender = max(max_sender, sender)
|
||||
for location, data in locations.items():
|
||||
receiver = data[1]
|
||||
if receiver < 1 or receiver > MAX_PLAYER_ID:
|
||||
raise ValueError(f"Invalid player id {receiver} for item")
|
||||
count += 1
|
||||
sender_count += 1
|
||||
|
||||
if not sender_count:
|
||||
raise ValueError(f"Rejecting game with 0 players")
|
||||
|
||||
if sender_count != max_sender:
|
||||
# we assume player 0 will never have locations
|
||||
raise ValueError("Player IDs not continuous")
|
||||
|
||||
if not count:
|
||||
warnings.warn("Game has no locations")
|
||||
|
||||
# allocate the arrays and invalidate index (0xff...)
|
||||
self.entries = <LocationEntry*>self._mem.alloc(count, sizeof(LocationEntry))
|
||||
self.sender_index = <IndexEntry*>self._mem.alloc(max_sender + 1, sizeof(IndexEntry))
|
||||
self._raw_proxies = <PyObject**>self._mem.alloc(max_sender + 1, sizeof(PyObject*))
|
||||
|
||||
# build entries and index
|
||||
cdef size_t i = 0
|
||||
for sender, locations in sorted(locations_dict.items()):
|
||||
self.sender_index[sender].start = i
|
||||
self.sender_index[sender].count = 0
|
||||
# Sorting locations here makes it possible to write a faster lookup without an additional index.
|
||||
for location, data in sorted(locations.items()):
|
||||
self.entries[i].sender = sender
|
||||
self.entries[i].location = location
|
||||
self.entries[i].item = data[0]
|
||||
self.entries[i].receiver = data[1]
|
||||
if len(data) > 2:
|
||||
self.entries[i].flags = data[2] # initialized to 0 during alloc
|
||||
# Ignoring extra data. warn?
|
||||
self.sender_index[sender].count += 1
|
||||
i += 1
|
||||
|
||||
# build pyobject caches
|
||||
self._proxies.append(None) # player 0
|
||||
assert self.sender_index[0].count == 0
|
||||
for i in range(1, max_sender + 1):
|
||||
assert self.sender_index[i].count == 0 or (
|
||||
self.sender_index[i].start < count and
|
||||
self.sender_index[i].start + self.sender_index[i].count <= count)
|
||||
key = i # allocate python integer
|
||||
proxy = PlayerLocationProxy(self, i)
|
||||
self._keys.append(key)
|
||||
self._items.append((key, proxy))
|
||||
self._proxies.append(proxy)
|
||||
self._raw_proxies[i] = <PyObject*>proxy
|
||||
|
||||
self.sender_index_size = max_sender + 1
|
||||
self.entry_count = count
|
||||
self._len = sender_count
|
||||
|
||||
# fake dict access
|
||||
def __len__(self) -> int:
|
||||
return self._len
|
||||
|
||||
def __iter__(self) -> Iterator[int]:
|
||||
return self._keys.__iter__()
|
||||
|
||||
def __getitem__(self, key: int) -> Any:
|
||||
# figure out if player actually exists in the multidata and return a proxy
|
||||
cdef size_t i = key # NOTE: this may raise TypeError
|
||||
if i < 1 or i >= self.sender_index_size:
|
||||
raise KeyError(key)
|
||||
return <object>self._raw_proxies[key]
|
||||
|
||||
T = TypeVar('T')
|
||||
|
||||
def get(self, key: int, default: T) -> Union[PlayerLocationProxy, T]:
|
||||
# calling into self.__getitem__ here is slow, but this is not used in MultiServer
|
||||
try:
|
||||
return self[key]
|
||||
except KeyError:
|
||||
return default
|
||||
|
||||
def items(self) -> Iterable[Tuple[int, PlayerLocationProxy]]:
|
||||
return self._items
|
||||
|
||||
# specialized accessors
|
||||
def find_item(self, slots: Set[int], seeked_item_id: int) -> Generator[Tuple[int, int, int, int, int], None, None]:
|
||||
cdef ap_id_t item = seeked_item_id
|
||||
cdef ap_player_t receiver
|
||||
cdef std_set[ap_player_t] receivers
|
||||
cdef size_t slot_count = len(slots)
|
||||
if slot_count == 1:
|
||||
# specialized implementation for single slot
|
||||
receiver = list(slots)[0]
|
||||
with nogil:
|
||||
for entry in self.entries[:self.entry_count]:
|
||||
if entry.item == item and entry.receiver == receiver:
|
||||
with gil:
|
||||
yield entry.sender, entry.location, entry.item, entry.receiver, entry.flags
|
||||
elif slot_count:
|
||||
# generic implementation with lookup in set
|
||||
for receiver in slots:
|
||||
receivers.insert(receiver)
|
||||
with nogil:
|
||||
for entry in self.entries[:self.entry_count]:
|
||||
if entry.item == item and receivers.count(entry.receiver):
|
||||
with gil:
|
||||
yield entry.sender, entry.location, entry.item, entry.receiver, entry.flags
|
||||
|
||||
def get_for_player(self, slot: int) -> Dict[int, Set[int]]:
|
||||
cdef ap_player_t receiver = slot
|
||||
all_locations: Dict[int, Set[int]] = {}
|
||||
with nogil:
|
||||
for entry in self.entries[:self.entry_count]:
|
||||
if entry.receiver == receiver:
|
||||
with gil:
|
||||
sender: int = entry.sender
|
||||
if sender not in all_locations:
|
||||
all_locations[sender] = set()
|
||||
all_locations[sender].add(entry.location)
|
||||
return all_locations
|
||||
|
||||
if TYPE_CHECKING:
|
||||
State = Dict[Tuple[int, int], Set[int]]
|
||||
else:
|
||||
State = Union[Tuple[int, int], Set[int], defaultdict]
|
||||
|
||||
def get_checked(self, state: State, team: int, slot: int) -> List[int]:
|
||||
# This used to validate checks actually exist. A remnant from the past.
|
||||
# If the order of locations becomes relevant at some point, we could not do sorted(set), so leaving it.
|
||||
cdef set checked = state[team, slot]
|
||||
|
||||
if not len(checked):
|
||||
# Skips loop if none have been checked.
|
||||
# This optimizes the case where everyone connects to a fresh game at the same time.
|
||||
return []
|
||||
|
||||
# Unless the set is close to empty, it's cheaper to use the python set directly, so we do that.
|
||||
cdef LocationEntry* entry
|
||||
cdef ap_player_t sender = slot
|
||||
cdef size_t start = self.sender_index[sender].start
|
||||
cdef size_t count = self.sender_index[sender].count
|
||||
return [entry.location for
|
||||
entry in self.entries[start:start+count] if
|
||||
entry.location in checked]
|
||||
|
||||
def get_missing(self, state: State, team: int, slot: int) -> List[int]:
|
||||
cdef LocationEntry* entry
|
||||
cdef ap_player_t sender = slot
|
||||
cdef size_t start = self.sender_index[sender].start
|
||||
cdef size_t count = self.sender_index[sender].count
|
||||
cdef set checked = state[team, slot]
|
||||
if not len(checked):
|
||||
# Skip `in` if none have been checked.
|
||||
# This optimizes the case where everyone connects to a fresh game at the same time.
|
||||
return [entry.location for
|
||||
entry in self.entries[start:start + count]]
|
||||
else:
|
||||
# Unless the set is close to empty, it's cheaper to use the python set directly, so we do that.
|
||||
return [entry.location for
|
||||
entry in self.entries[start:start + count] if
|
||||
entry.location not in checked]
|
||||
|
||||
def get_remaining(self, state: State, team: int, slot: int) -> List[int]:
|
||||
cdef LocationEntry* entry
|
||||
cdef ap_player_t sender = slot
|
||||
cdef size_t start = self.sender_index[sender].start
|
||||
cdef size_t count = self.sender_index[sender].count
|
||||
cdef set checked = state[team, slot]
|
||||
return sorted([entry.item for
|
||||
entry in self.entries[start:start+count] if
|
||||
entry.location not in checked])
|
||||
|
||||
|
||||
@cython.internal # unsafe. disable direct import
|
||||
cdef class PlayerLocationProxy:
|
||||
cdef LocationStore _store
|
||||
cdef size_t _player
|
||||
cdef object _len
|
||||
|
||||
def __init__(self, store: LocationStore, player: int) -> None:
|
||||
self._store = store
|
||||
self._player = player
|
||||
self._len = self._store.sender_index[self._player].count
|
||||
|
||||
def __len__(self) -> int:
|
||||
return self._store.sender_index[self._player].count
|
||||
|
||||
def __iter__(self) -> Generator[int, None, None]:
|
||||
cdef LocationEntry* entry
|
||||
cdef size_t i
|
||||
cdef size_t off = self._store.sender_index[self._player].start
|
||||
for i in range(self._store.sender_index[self._player].count):
|
||||
entry = self._store.entries + off + i
|
||||
yield entry.location
|
||||
|
||||
cdef LocationEntry* _get(self, ap_id_t loc):
|
||||
# This requires locations to be sorted.
|
||||
# This is always going to be slower than a pure python dict, because constructing the result tuple takes as long
|
||||
# as the search in a python dict, which stores a pointer to an existing tuple.
|
||||
cdef LocationEntry* entry = NULL
|
||||
# binary search
|
||||
cdef size_t l = self._store.sender_index[self._player].start
|
||||
cdef size_t r = l + self._store.sender_index[self._player].count
|
||||
cdef size_t m
|
||||
while l < r:
|
||||
m = (l + r) // 2
|
||||
entry = self._store.entries + m
|
||||
if entry.location < loc:
|
||||
l = m + 1
|
||||
else:
|
||||
r = m
|
||||
if entry: # count != 0
|
||||
entry = self._store.entries + l
|
||||
if entry.location == loc:
|
||||
return entry
|
||||
return NULL
|
||||
|
||||
def __getitem__(self, key: int) -> Tuple[int, int, int]:
|
||||
cdef LocationEntry* entry = self._get(key)
|
||||
if entry:
|
||||
return entry.item, entry.receiver, entry.flags
|
||||
raise KeyError(f"No location {key} for player {self._player}")
|
||||
|
||||
T = TypeVar('T')
|
||||
|
||||
def get(self, key: int, default: T) -> Union[Tuple[int, int, int], T]:
|
||||
cdef LocationEntry* entry = self._get(key)
|
||||
if entry:
|
||||
return entry.item, entry.receiver, entry.flags
|
||||
return default
|
||||
|
||||
def items(self) -> Generator[Tuple[int, Tuple[int, int, int]], None, None]:
|
||||
cdef LocationEntry* entry
|
||||
start = self._store.sender_index[self._player].start
|
||||
count = self._store.sender_index[self._player].count
|
||||
for entry in self._store.entries[start:start+count]:
|
||||
yield entry.location, (entry.item, entry.receiver, entry.flags)
|
||||
8
_speedups.pyxbld
Normal file
8
_speedups.pyxbld
Normal file
@@ -0,0 +1,8 @@
|
||||
# This file is required to get pyximport to work with C++.
|
||||
# Switching from std::set to a pure C implementation is still on the table to simplify everything.
|
||||
|
||||
def make_ext(modname, pyxfilename):
|
||||
from distutils.extension import Extension
|
||||
return Extension(name=modname,
|
||||
sources=[pyxfilename],
|
||||
language='c++')
|
||||
@@ -43,13 +43,13 @@
|
||||
|
||||
|
||||
local socket = require("socket")
|
||||
local udp = socket.socket.udp()
|
||||
udp = socket.socket.udp()
|
||||
require('common')
|
||||
|
||||
udp:setsockname('127.0.0.1', 55355)
|
||||
udp:settimeout(0)
|
||||
|
||||
while true do
|
||||
function on_vblank()
|
||||
-- Attempt to lessen the CPU load by only polling the UDP socket every x frames.
|
||||
-- x = 10 is entirely arbitrary, very little thought went into it.
|
||||
-- We could try to make use of client.get_approx_framerate() here, but the values returned
|
||||
@@ -112,6 +112,7 @@ while true do
|
||||
for _, v in ipairs(mem) do
|
||||
hex_string = hex_string .. string.format("%02X ", v)
|
||||
end
|
||||
|
||||
hex_string = hex_string:sub(1, -2) -- Hang head in shame, remove last " "
|
||||
local reply = string.format("%s %02x %s\n", command, address, hex_string)
|
||||
udp:sendto(reply, msg_or_ip, port_or_nil)
|
||||
@@ -135,6 +136,10 @@ while true do
|
||||
udp:sendto(reply, msg_or_ip, port_or_nil)
|
||||
end
|
||||
end
|
||||
|
||||
emu.frameadvance()
|
||||
end
|
||||
|
||||
event.onmemoryexecute(on_vblank, 0x40, "ap_connector_vblank")
|
||||
|
||||
while true do
|
||||
emu.yield()
|
||||
end
|
||||
|
||||
@@ -46,10 +46,10 @@ function get_socket_path()
|
||||
local pwd = (io.popen and io.popen("cd"):read'*l') or "."
|
||||
return pwd .. "/" .. arch .. "/socket-" .. the_os .. "-" .. get_lua_version() .. "." .. ext
|
||||
end
|
||||
|
||||
local lua_version = get_lua_version()
|
||||
local socket_path = get_socket_path()
|
||||
local socket = assert(package.loadlib(socket_path, "luaopen_socket_core"))()
|
||||
|
||||
local event = event
|
||||
-- http://lua-users.org/wiki/ModulesTutorial
|
||||
local M = {}
|
||||
if setfenv then
|
||||
@@ -59,6 +59,20 @@ else
|
||||
end
|
||||
|
||||
M.socket = socket
|
||||
-- Bizhawk <= 2.8 has an issue where resetting the lua doesn't close the socket
|
||||
-- ...to get around this, we register an exit handler to close the socket first
|
||||
if lua_version == '5-1' then
|
||||
local old_udp = socket.udp
|
||||
function udp(self)
|
||||
s = old_udp(self)
|
||||
function close_socket(self)
|
||||
s:close()
|
||||
end
|
||||
event.onexit(close_socket)
|
||||
return s
|
||||
end
|
||||
socket.udp = udp
|
||||
end
|
||||
|
||||
-----------------------------------------------------------------------------
|
||||
-- Exported auxiliar functions
|
||||
|
||||
166
docs/CODEOWNERS
Normal file
166
docs/CODEOWNERS
Normal file
@@ -0,0 +1,166 @@
|
||||
# Archipelago World Code Owners / Maintainers Document
|
||||
#
|
||||
# This file is used to notate the current "owners" or "maintainers" of any currently merged world folder. For any pull
|
||||
# requests that modify these worlds, a code owner must approve the PR in addition to a core maintainer. This is not to
|
||||
# be used for files/folders outside the /worlds folder, those will always need sign off from a core maintainer.
|
||||
#
|
||||
# All usernames must be GitHub usernames (and are case sensitive).
|
||||
|
||||
###################
|
||||
## Active Worlds ##
|
||||
###################
|
||||
|
||||
# Adventure
|
||||
/worlds/adventure/ @JusticePS
|
||||
|
||||
# A Link to the Past
|
||||
/worlds/alttp/ @Berserker66
|
||||
|
||||
# ArchipIDLE
|
||||
/worlds/archipidle/ @LegendaryLinux
|
||||
|
||||
# Sudoku (BK Sudoku)
|
||||
/worlds/bk_sudoku/ @Jarno458
|
||||
|
||||
# Blasphemous
|
||||
/worlds/blasphemous/ @TRPG0
|
||||
|
||||
# Bumper Stickers
|
||||
/worlds/bumpstik/ @FelicitusNeko
|
||||
|
||||
# ChecksFinder
|
||||
/worlds/checksfinder/ @jonloveslegos
|
||||
|
||||
# Clique
|
||||
/worlds/clique/ @ThePhar
|
||||
|
||||
# Dark Souls III
|
||||
/worlds/dark_souls_3/ @Marechal-L
|
||||
|
||||
# Donkey Kong Country 3
|
||||
/worlds/dkc3/ @PoryGone
|
||||
|
||||
# DLCQuest
|
||||
/worlds/dlcquest/ @axe-y @agilbert1412
|
||||
|
||||
# DOOM 1993
|
||||
/worlds/doom_1993/ @Daivuk
|
||||
|
||||
# Factorio
|
||||
/worlds/factorio/ @Berserker66
|
||||
|
||||
# Final Fantasy
|
||||
/worlds/ff1/ @jtoyoda
|
||||
|
||||
# Hollow Knight
|
||||
/worlds/hk/ @BadMagic100 @ThePhar
|
||||
|
||||
# Hylics 2
|
||||
/worlds/hylics2/ @TRPG0
|
||||
|
||||
# Kingdom Hearts 2
|
||||
/worlds/kh2/ @JaredWeakStrike
|
||||
|
||||
# Links Awakening DX
|
||||
/worlds/ladx/ @zig-for
|
||||
|
||||
# Lufia II Ancient Cave
|
||||
/worlds/lufia2ac/ @el-u
|
||||
/worlds/lufia2ac/docs/ @wordfcuk @el-u
|
||||
|
||||
# Meritous
|
||||
/worlds/meritous/ @FelicitusNeko
|
||||
|
||||
# The Messenger
|
||||
/worlds/messenger/ @alwaysintreble
|
||||
|
||||
# Minecraft
|
||||
/worlds/minecraft/ @KonoTyran @espeon65536
|
||||
|
||||
# MegaMan Battle Network 3
|
||||
/worlds/mmbn3/ @digiholic
|
||||
|
||||
# Muse Dash
|
||||
/worlds/musedash/ @DeamonHunter
|
||||
|
||||
# Noita
|
||||
/worlds/noita/ @ScipioWright @heinermann
|
||||
|
||||
# Ocarina of Time
|
||||
/worlds/oot/ @espeon65536
|
||||
|
||||
# Overcooked! 2
|
||||
/worlds/overcooked2/ @toasterparty
|
||||
|
||||
# Pokemon Red and Blue
|
||||
/worlds/pokemon_rb/ @Alchav
|
||||
|
||||
# Raft
|
||||
/worlds/raft/ @SunnyBat
|
||||
|
||||
# Rogue Legacy
|
||||
/worlds/rogue_legacy/ @ThePhar
|
||||
|
||||
# Risk of Rain 2
|
||||
/worlds/ror2/ @kindasneaki
|
||||
|
||||
# Sonic Adventure 2 Battle
|
||||
/worlds/sa2b/ @PoryGone @RaspberrySpace
|
||||
|
||||
# Starcraft 2 Wings of Liberty
|
||||
/worlds/sc2wol/ @Ziktofel
|
||||
|
||||
# Super Metroid
|
||||
/worlds/sm/ @lordlou
|
||||
|
||||
# Super Mario 64
|
||||
/worlds/sm64ex/ @N00byKing
|
||||
|
||||
# Super Mario World
|
||||
/worlds/smw/ @PoryGone
|
||||
|
||||
# SMZ3
|
||||
/worlds/smz3/ @lordlou
|
||||
|
||||
# Secret of Evermore
|
||||
/worlds/soe/ @black-sliver
|
||||
|
||||
# Slay the Spire
|
||||
/worlds/spire/ @KonoTyran
|
||||
|
||||
# Stardew Valley
|
||||
/worlds/stardew_valley/ @agilbert1412
|
||||
|
||||
# Subnautica
|
||||
/worlds/subnautica/ @Berserker66
|
||||
|
||||
# Terraria
|
||||
/worlds/terraria/ @Seldom-SE
|
||||
|
||||
# Timespinner
|
||||
/worlds/timespinner/ @Jarno458
|
||||
|
||||
# The Legend of Zelda (1)
|
||||
/worlds/tloz/ @Rosalie-A @t3hf1gm3nt
|
||||
|
||||
# Undertale
|
||||
/worlds/undertale/ @jonloveslegos
|
||||
|
||||
# VVVVVV
|
||||
/worlds/v6/ @N00byKing
|
||||
|
||||
# Wargroove
|
||||
/worlds/wargroove/ @FlySniper
|
||||
|
||||
# The Witness
|
||||
/worlds/witness/ @NewSoupVi @blastron
|
||||
|
||||
# Zillion
|
||||
/worlds/zillion/ @beauxq
|
||||
|
||||
##################################
|
||||
## Disabled Unmaintained Worlds ##
|
||||
##################################
|
||||
|
||||
# Ori and the Blind Forest
|
||||
# /worlds_disabled/oribf/ <Unmaintained>
|
||||
@@ -1,7 +1,7 @@
|
||||
# apworld Specification
|
||||
|
||||
Archipelago depends on worlds to provide game-specific details like items, locations and output generation.
|
||||
Those are located in the `worlds/` folder (source) or `<insall dir>/lib/worlds/` (when installed).
|
||||
Those are located in the `worlds/` folder (source) or `<install dir>/lib/worlds/` (when installed).
|
||||
See [world api.md](world%20api.md) for details.
|
||||
|
||||
apworld provides a way to package and ship a world that is not part of the main distribution by placing a `*.apworld`
|
||||
|
||||
187
docs/settings api.md
Normal file
187
docs/settings api.md
Normal file
@@ -0,0 +1,187 @@
|
||||
# Archipelago Settings API
|
||||
|
||||
The settings API describes how to use installation-wide config and let the user configure them, like paths, etc. using
|
||||
host.yaml. For the player settings / player yamls see [options api.md](options api.md).
|
||||
|
||||
The settings API replaces `Utils.get_options()` and `Utils.get_default_options()`
|
||||
as well as the predefined `host.yaml` in the repository.
|
||||
|
||||
For backwards compatibility with APWorlds, some interfaces are kept for now and will produce a warning when being used.
|
||||
|
||||
|
||||
## Config File
|
||||
|
||||
Settings use options.yaml (manual override), if that exists, or host.yaml (the default) otherwise.
|
||||
The files are searched for in the current working directory, if different from install directory, and in `user_path`,
|
||||
which either points to the installation directory, if writable, or to %home%/Archipelago otherwise.
|
||||
|
||||
**Examples:**
|
||||
* C:\Program Data\Archipelago\options.yaml
|
||||
* C:\Program Data\Archipelago\host.yaml
|
||||
* path\to\code\repository\host.yaml
|
||||
* ~/Archipelago/host.yaml
|
||||
|
||||
Using the settings API, AP can update the config file or create a new one with default values and comments,
|
||||
if it does not exist.
|
||||
|
||||
|
||||
## Global Settings
|
||||
|
||||
All non-world-specific settings are defined directly in settings.py.
|
||||
Each value needs to have a default. If the default should be `None`, define it as `typing.Optional` and assign `None`.
|
||||
|
||||
To access a "global" config value, with correct typing, use one of
|
||||
```python
|
||||
from settings import get_settings, GeneralOptions, FolderPath
|
||||
from typing import cast
|
||||
|
||||
x = get_settings().general_options.output_path
|
||||
y = cast(GeneralOptions, get_settings()["general_options"]).output_path
|
||||
z = cast(FolderPath, get_settings()["general_options"]["output_path"])
|
||||
```
|
||||
|
||||
|
||||
## World Settings
|
||||
|
||||
Worlds can define the top level key to use by defining `settings_key: ClassVar[str]` in their World class.
|
||||
It defaults to `{folder_name}_options` if undefined, i.e. `worlds/factorio/...` defaults to `factorio_options`.
|
||||
|
||||
Worlds define the layout of their config section using type annotation of the variable `settings` in the class.
|
||||
The type has to inherit from `settings.Group`. Each value in the config can have a comment by subclassing a built-in
|
||||
type. Some helper types are defined in `settings.py`, see [Types](#Types) for a list.```
|
||||
|
||||
Inside the class code, you can then simply use `self.settings.rom_file` to get the value.
|
||||
In case of paths they will automatically be read as absolute file paths. No need to use user_path or local_path.
|
||||
|
||||
```python
|
||||
import settings
|
||||
from worlds.AutoWorld import World
|
||||
|
||||
|
||||
class MyGameSettings(settings.Group):
|
||||
class RomFile(settings.SNESRomPath):
|
||||
"""Description that is put into host.yaml"""
|
||||
description = "My Game US v1.0 ROM File" # displayed in the file browser
|
||||
copy_to = "MyGame.sfc" # instead of storing the path, copy to AP dir
|
||||
md5s = ["..."]
|
||||
|
||||
rom_file: RomFile = RomFile("MyGame.sfc") # definition and default value
|
||||
|
||||
|
||||
class MyGameWorld(World):
|
||||
...
|
||||
settings: MyGameSettings
|
||||
...
|
||||
|
||||
def something(self):
|
||||
pass # use self.settings.rom_file here
|
||||
```
|
||||
|
||||
|
||||
## Types
|
||||
|
||||
When writing the host.yaml, the code will down cast the values to builtins.
|
||||
When reading the host.yaml, the code will upcast the values to what is defined in the type annotations.
|
||||
E.g. an IntEnum becomes int when saving and will construct the IntEnum when loading.
|
||||
|
||||
Types that can not be down cast to / up cast from a builtin can not be used except for Group, which will be converted
|
||||
to/from a dict.
|
||||
`bool` is a special case, see settings.py: ServerOptions.disable_item_cheat for an example.
|
||||
|
||||
Below are some predefined types that can be used if they match your requirements:
|
||||
|
||||
|
||||
### Group
|
||||
|
||||
A section / dict in the config file. Behaves similar to a dataclass.
|
||||
Type annotation and default assignment define how loading, saving and default values behave.
|
||||
It can be accessed using attributes or as a dict: `group["a"]` is equivalent to `group.a`.
|
||||
|
||||
In worlds, this should only be used for the top level to avoid issues when upgrading/migrating.
|
||||
|
||||
|
||||
### Bool
|
||||
|
||||
Since `bool` can not be subclassed, use the `settings.Bool` helper in a `typing.Union` to get a comment in host.yaml.
|
||||
|
||||
```python
|
||||
import settings
|
||||
import typing
|
||||
|
||||
class MySettings(settings.Group):
|
||||
class MyBool(settings.Bool):
|
||||
"""Doc string"""
|
||||
|
||||
my_value: typing.Union[MyBool, bool] = True
|
||||
```
|
||||
|
||||
### UserFilePath
|
||||
|
||||
Path to a single file. Automatically resolves as user_path:
|
||||
Source folder or AP install path on Windows. ~/Archipelago for the AppImage.
|
||||
Will open a file browser if the file is missing when in GUI mode.
|
||||
|
||||
#### class method validate(cls, path: str)
|
||||
|
||||
Override this and raise ValueError if validation fails.
|
||||
Checks the file against [md5s](#md5s) by default.
|
||||
|
||||
#### is_exe: bool
|
||||
|
||||
Resolves to an executable (varying file extension based on platform)
|
||||
|
||||
#### description: Optional\[str\]
|
||||
|
||||
Human-readable name to use in file browser
|
||||
|
||||
#### copy_to: Optional\[str\]
|
||||
|
||||
Instead of storing the path, copy the file.
|
||||
|
||||
#### md5s: List[Union[str, bytes]]
|
||||
|
||||
Provide md5 hashes as hex digests or raw bytes for automatic validation.
|
||||
|
||||
|
||||
### UserFolderPath
|
||||
|
||||
Same as [UserFilePath](#UserFilePath), but for a folder instead of a file.
|
||||
|
||||
|
||||
### LocalFilePath
|
||||
|
||||
Same as [UserFilePath](#UserFilePath), but resolves as local_path:
|
||||
path inside the AP dir or Appimage even if read-only.
|
||||
|
||||
|
||||
### LocalFolderPath
|
||||
|
||||
Same as [LocalFilePath](#LocalFilePath), but for a folder instead of a file.
|
||||
|
||||
|
||||
### OptionalUserFilePath, OptionalUserFolderPath, OptionalLocalFilePath, OptionalLocalFolderPath
|
||||
|
||||
Same as UserFilePath, UserFolderPath, LocalFilePath, LocalFolderPath but does not open a file browser if missing.
|
||||
|
||||
|
||||
### SNESRomPath
|
||||
|
||||
Specialized [UserFilePath](#UserFilePath) that ignores an optional 512 byte header when validating.
|
||||
|
||||
|
||||
## Caveats
|
||||
|
||||
### Circular Imports
|
||||
|
||||
Because the settings are defined on import, code that runs on import can not use settings since that would result in
|
||||
circular / partial imports. Instead, the code should fetch from settings on demand during generation.
|
||||
|
||||
"Global" settings are populated immediately, while worlds settings are lazy loaded, so if really necessary,
|
||||
"global" settings could be used in global scope of worlds.
|
||||
|
||||
|
||||
### APWorld Backwards Compatibility
|
||||
|
||||
APWorlds that want to be compatible with both stable and dev versions, have two options:
|
||||
1. use the old Utils.get_options() API until Archipelago 0.4.2 is out
|
||||
2. add some sort of compatibility code to your world that mimics the new API
|
||||
@@ -22,8 +22,8 @@ allows using WebSockets.
|
||||
|
||||
## Coding style
|
||||
|
||||
AP follows all the PEPs. When in doubt use an IDE with coding style
|
||||
linter, for example PyCharm Community Edition.
|
||||
AP follows [style.md](https://github.com/ArchipelagoMW/Archipelago/blob/main/docs/style.md).
|
||||
When in doubt use an IDE with coding style linter, for example PyCharm Community Edition.
|
||||
|
||||
|
||||
## Docstrings
|
||||
@@ -44,7 +44,7 @@ class MyGameWorld(World):
|
||||
## Definitions
|
||||
|
||||
This section will cover various classes and objects you can use for your world.
|
||||
While some of the attributes and methods are mentioned here not all of them are,
|
||||
While some of the attributes and methods are mentioned here, not all of them are,
|
||||
but you can find them in `BaseClasses.py`.
|
||||
|
||||
### World Class
|
||||
@@ -56,11 +56,12 @@ game.
|
||||
### WebWorld Class
|
||||
|
||||
A `WebWorld` class contains specific attributes and methods that can be modified
|
||||
for your world specifically on the webhost.
|
||||
for your world specifically on the webhost:
|
||||
|
||||
`settings_page` which can be changed to a link instead of an AP generated settings page.
|
||||
`settings_page`, which can be changed to a link instead of an AP generated settings page.
|
||||
|
||||
`theme` to be used for your game specific AP pages. Available themes:
|
||||
|
||||
| dirt | grass (default) | grassFlowers | ice | jungle | ocean | partyTime | stone |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| <img src="img/theme_dirt.JPG" width="100"> | <img src="img/theme_grass.JPG" width="100"> | <img src="img/theme_grassFlowers.JPG" width="100"> | <img src="img/theme_ice.JPG" width="100"> | <img src="img/theme_jungle.JPG" width="100"> | <img src="img/theme_ocean.JPG" width="100"> | <img src="img/theme_partyTime.JPG" width="100"> | <img src="img/theme_stone.JPG" width="100"> |
|
||||
@@ -75,26 +76,30 @@ prefixed with the same string as defined here. Default already has 'en'.
|
||||
### MultiWorld Object
|
||||
|
||||
The `MultiWorld` object references the whole multiworld (all items and locations
|
||||
for all players) and is accessible through `self.world` inside a `World` object.
|
||||
for all players) and is accessible through `self.multiworld` inside a `World` object.
|
||||
|
||||
### Player
|
||||
|
||||
The player is just an integer in AP and is accessible through `self.player`
|
||||
inside a World object.
|
||||
inside a `World` object.
|
||||
|
||||
### Player Options
|
||||
|
||||
Players provide customized settings for their World in the form of yamls.
|
||||
Those are accessible through `self.world.<option_name>[self.player]`. A dict
|
||||
Those are accessible through `self.multiworld.<option_name>[self.player]`. A dict
|
||||
of valid options has to be provided in `self.option_definitions`. Options are automatically
|
||||
added to the `World` object for easy access.
|
||||
|
||||
### World Options
|
||||
### World Settings
|
||||
|
||||
Any AP installation can provide settings for a world, for example a ROM file,
|
||||
accessible through `Utils.get_options()['<world>_options']['<option>']`.
|
||||
Any AP installation can provide settings for a world, for example a ROM file, accessible through
|
||||
`self.settings.<setting_name>` or `cls.settings.<setting_name>` (new API)
|
||||
or `Utils.get_options()["<world>_options"]["<setting_name>"]` (deprecated).
|
||||
|
||||
Users can set those in their `host.yaml` file.
|
||||
Users can set those in their `host.yaml` file. Some settings may automatically open a file browser if a file is missing.
|
||||
|
||||
Refer to [settings api.md](https://github.com/ArchipelagoMW/Archipelago/blob/main/docs/settings%20api.md)
|
||||
for details.
|
||||
|
||||
### Locations
|
||||
|
||||
@@ -132,10 +137,13 @@ same ID. Name must not be numeric (has to contain at least 1 letter or symbol).
|
||||
Special items with ID `None` can mark events (read below).
|
||||
|
||||
Other classifications include
|
||||
* filler: a regular item or trash item
|
||||
* useful: generally quite useful, but not required for anything logical
|
||||
* trap: negative impact on the player
|
||||
* skip_balancing: add to progression to skip balancing; e.g. currency or tokens
|
||||
* `filler`: a regular item or trash item
|
||||
* `useful`: generally quite useful, but not required for anything logical
|
||||
* `trap`: negative impact on the player
|
||||
* `skip_balancing`: denotes that an item should not be moved to an earlier sphere for the purpose of balancing (to be
|
||||
combined with `progression`; see below)
|
||||
* `progression_skip_balancing`: the combination of `progression` and `skip_balancing`, i.e., a progression item that
|
||||
will not be moved around by progression balancing; used, e.g., for currency or tokens
|
||||
|
||||
### Events
|
||||
|
||||
@@ -159,10 +167,10 @@ or more event locations based on player options.
|
||||
|
||||
Regions are logical groups of locations that share some common access rules. If
|
||||
location logic is written from scratch, using regions greatly simplifies the
|
||||
definition and allow to somewhat easily implement things like entrance
|
||||
definition and allows to somewhat easily implement things like entrance
|
||||
randomizer in logic.
|
||||
|
||||
Regions have a list called `exits` which are `Entrance` objects representing
|
||||
Regions have a list called `exits`, which are `Entrance` objects representing
|
||||
transitions to other regions.
|
||||
|
||||
There has to be one special region "Menu" from which the logic unfolds. AP
|
||||
@@ -179,7 +187,7 @@ They can be static (regular logic) or be defined/connected during generation
|
||||
### Access Rules
|
||||
|
||||
An access rule is a function that returns `True` or `False` for a `Location` or
|
||||
`Entrance` based on the the current `state` (items that can be collected).
|
||||
`Entrance` based on the current `state` (items that can be collected).
|
||||
|
||||
### Item Rules
|
||||
|
||||
@@ -196,14 +204,14 @@ the `/worlds` directory. The starting point for the package is `__init__.py`.
|
||||
Conventionally, your world class is placed in that file.
|
||||
|
||||
World classes must inherit from the `World` class in `/worlds/AutoWorld.py`,
|
||||
which can be imported as `worlds.AutoWorld.World` from your package.
|
||||
which can be imported as `from worlds.AutoWorld import World` from your package.
|
||||
|
||||
AP will pick up your world automatically due to the `AutoWorld` implementation.
|
||||
|
||||
### Requirements
|
||||
|
||||
If your world needs specific python packages, they can be listed in
|
||||
`world/[world_name]/requirements.txt`. ModuleUpdate.py will automatically
|
||||
`worlds/<world_name>/requirements.txt`. ModuleUpdate.py will automatically
|
||||
pick up and install them.
|
||||
|
||||
See [pip documentation](https://pip.pypa.io/en/stable/cli/pip_install/#requirements-file-format).
|
||||
@@ -214,7 +222,7 @@ AP will only import the `__init__.py`. Depending on code size it makes sense to
|
||||
use multiple files and use relative imports to access them.
|
||||
|
||||
e.g. `from .Options import mygame_options` from your `__init__.py` will load
|
||||
`world/[world_name]/Options.py` and make its `mygame_options` accesible.
|
||||
`worlds/<world_name>/Options.py` and make its `mygame_options` accessible.
|
||||
|
||||
When imported names pile up it may be easier to use `from . import Options`
|
||||
and access the variable as `Options.mygame_options`.
|
||||
@@ -225,12 +233,12 @@ function, see [apworld specification.md](apworld%20specification.md).
|
||||
|
||||
### Your Item Type
|
||||
|
||||
Each world uses its own subclass of `BaseClasses.Item`. The constuctor can be
|
||||
Each world uses its own subclass of `BaseClasses.Item`. The constructor can be
|
||||
overridden to attach additional data to it, e.g. "price in shop".
|
||||
Since the constructor is only ever called from your code, you can add whatever
|
||||
arguments you like to the constructor.
|
||||
|
||||
In its simplest form we only set the game name and use the default constuctor
|
||||
In its simplest form we only set the game name and use the default constructor
|
||||
```python
|
||||
from BaseClasses import Item
|
||||
|
||||
@@ -265,7 +273,7 @@ Each option has its own class, inherits from a base option type, has a docstring
|
||||
to describe it and a `display_name` property for display on the website and in
|
||||
spoiler logs.
|
||||
|
||||
The actual name as used in the yaml is defined in a `dict[str, Option]`, that is
|
||||
The actual name as used in the yaml is defined in a `Dict[str, AssembleOptions]`, that is
|
||||
assigned to the world under `self.option_definitions`.
|
||||
|
||||
Common option types are `Toggle`, `DefaultOnToggle`, `Choice`, `Range`.
|
||||
@@ -326,10 +334,10 @@ class FixXYZGlitch(Toggle):
|
||||
display_name = "Fix XYZ Glitch"
|
||||
|
||||
# By convention we call the options dict variable `<world>_options`.
|
||||
mygame_options: typing.Dict[str, type(Option)] = {
|
||||
mygame_options: typing.Dict[str, AssembleOptions] = {
|
||||
"difficulty": Difficulty,
|
||||
"final_boss_hp": FinalBossHP,
|
||||
"fix_xyz_glitch": FixXYZGlitch
|
||||
"fix_xyz_glitch": FixXYZGlitch,
|
||||
}
|
||||
```
|
||||
```python
|
||||
@@ -349,27 +357,39 @@ class MyGameWorld(World):
|
||||
```python
|
||||
# world/mygame/__init__.py
|
||||
|
||||
import settings
|
||||
import typing
|
||||
from .Options import mygame_options # the options we defined earlier
|
||||
from .Items import mygame_items # data used below to add items to the World
|
||||
from .Locations import mygame_locations # same as above
|
||||
from worlds.AutoWorld import World
|
||||
from BaseClasses import Region, Location, Entrance, Item, RegionType, ItemClassification
|
||||
from Utils import get_options, output_path
|
||||
|
||||
|
||||
class MyGameItem(Item): # or from Items import MyGameItem
|
||||
game = "My Game" # name of the game/world this item is from
|
||||
|
||||
|
||||
class MyGameLocation(Location): # or from Locations import MyGameLocation
|
||||
game = "My Game" # name of the game/world this location is in
|
||||
|
||||
|
||||
class MyGameSettings(settings.Group):
|
||||
class RomFile(settings.SNESRomPath):
|
||||
"""Insert help text for host.yaml here."""
|
||||
|
||||
rom_file: RomFile = RomFile("MyGame.sfc")
|
||||
|
||||
|
||||
class MyGameWorld(World):
|
||||
"""Insert description of the world/game here."""
|
||||
game = "My Game" # name of the game/world
|
||||
option_definitions = mygame_options # options the player can set
|
||||
settings: typing.ClassVar[MyGameSettings] # will be automatically assigned from type hint
|
||||
topology_present = True # show path to required location checks in spoiler
|
||||
|
||||
# ID of first item and location, could be hard-coded but code may be easier
|
||||
# to read with this as a propery.
|
||||
# to read with this as a property.
|
||||
base_id = 1234
|
||||
# Instead of dynamic numbering, IDs could be part of data.
|
||||
|
||||
@@ -384,7 +404,7 @@ class MyGameWorld(World):
|
||||
# Items can be grouped using their names to allow easy checking if any item
|
||||
# from that group has been collected. Group names can also be used for !hint
|
||||
item_name_groups = {
|
||||
"weapons": {"sword", "lance"}
|
||||
"weapons": {"sword", "lance"},
|
||||
}
|
||||
```
|
||||
|
||||
@@ -398,7 +418,7 @@ The world has to provide the following things for generation
|
||||
* locations placed inside those regions
|
||||
* a `def create_item(self, item: str) -> MyGameItem` to create any item on demand
|
||||
* applying `self.multiworld.push_precollected` for start inventory
|
||||
* `required_client_version: Tuple(int, int, int)`
|
||||
* `required_client_version: Tuple[int, int, int]`
|
||||
Optional client version as tuple of 3 ints to make sure the client is compatible to
|
||||
this world (e.g. implements all required features) when connecting.
|
||||
|
||||
@@ -496,30 +516,28 @@ def create_items(self) -> None:
|
||||
def create_regions(self) -> None:
|
||||
# Add regions to the multiworld. "Menu" is the required starting point.
|
||||
# Arguments to Region() are name, player, world, and optionally hint_text
|
||||
r = Region("Menu", self.player, self.multiworld)
|
||||
# Set Region.exits to a list of entrances that are reachable from region
|
||||
r.exits = [Entrance(self.player, "New game", r)] # or use r.exits.append
|
||||
# Append region to MultiWorld's regions
|
||||
self.multiworld.regions.append(r) # or use += [r...]
|
||||
menu_region = Region("Menu", self.player, self.multiworld)
|
||||
self.multiworld.regions.append(menu_region) # or use += [menu_region...]
|
||||
|
||||
r = Region("Main Area", self.player, self.multiworld)
|
||||
main_region = Region("Main Area", self.player, self.multiworld)
|
||||
# Add main area's locations to main area (all but final boss)
|
||||
r.locations = [MyGameLocation(self.player, location.name,
|
||||
self.location_name_to_id[location.name], r)]
|
||||
r.exits = [Entrance(self.player, "Boss Door", r)]
|
||||
self.multiworld.regions.append(r)
|
||||
main_region.add_locations(main_region_locations, MyGameLocation)
|
||||
# or
|
||||
# main_region.locations = \
|
||||
# [MyGameLocation(self.player, location_name, self.location_name_to_id[location_name], main_region]
|
||||
self.multiworld.regions.append(main_region)
|
||||
|
||||
r = Region("Boss Room", self.player, self.multiworld)
|
||||
# add event to Boss Room
|
||||
r.locations = [MyGameLocation(self.player, "Final Boss", None, r)]
|
||||
self.multiworld.regions.append(r)
|
||||
|
||||
# If entrances are not randomized, they should be connected here, otherwise
|
||||
# they can also be connected at a later stage.
|
||||
self.multiworld.get_entrance("New Game", self.player)
|
||||
.connect(self.multiworld.get_region("Main Area", self.player))
|
||||
self.multiworld.get_entrance("Boss Door", self.player)
|
||||
.connect(self.multiworld.get_region("Boss Room", self.player))
|
||||
boss_region = Region("Boss Room", self.player, self.multiworld)
|
||||
# Add event to Boss Room
|
||||
boss_region.locations.append(MyGameLocation(self.player, "Final Boss", None, boss_region))
|
||||
|
||||
# If entrances are not randomized, they should be connected here,
|
||||
# otherwise they can also be connected at a later stage.
|
||||
# Create Entrances and connect the Regions
|
||||
menu_region.connect(main_region) # connects the "Menu" and "Main Area", can also pass a rule
|
||||
# or
|
||||
main_region.add_exits({"Boss Room": "Boss Door"}, {"Boss Room": lambda state: state.has("Sword", self.player)})
|
||||
# Connects the "Main Area" and "Boss Room" regions, and places a rule requiring the "Sword" item to traverse
|
||||
|
||||
# If setting location access rules from data is easier here, set_rules can
|
||||
# possibly omitted.
|
||||
@@ -573,7 +591,7 @@ def set_rules(self) -> None:
|
||||
# require one item from an item group
|
||||
add_rule(self.multiworld.get_location("Chest3", self.player),
|
||||
lambda state: state.has_group("weapons", self.player))
|
||||
# state also has .item_count() for items, .has_any() and.has_all() for sets
|
||||
# state also has .item_count() for items, .has_any() and .has_all() for sets
|
||||
# and .count_group() for groups
|
||||
# set_rule is likely to be a bit faster than add_rule
|
||||
|
||||
@@ -611,7 +629,7 @@ public members with `mygame_`.
|
||||
More advanced uses could be to add additional variables to the state object,
|
||||
override `World.collect(self, state, item)` and `remove(self, state, item)`
|
||||
to update the state object, and check those added variables in added methods.
|
||||
Please do this with caution and only when neccessary.
|
||||
Please do this with caution and only when necessary.
|
||||
|
||||
#### Sample
|
||||
|
||||
@@ -623,7 +641,7 @@ from worlds.AutoWorld import LogicMixin
|
||||
class MyGameLogic(LogicMixin):
|
||||
def mygame_has_key(self, player: int):
|
||||
# Arguments above are free to choose
|
||||
# MultiWorld can be accessed through self.world, explicitly passing in
|
||||
# MultiWorld can be accessed through self.multiworld, explicitly passing in
|
||||
# MyGameWorld instance for easy options access is also a valid approach
|
||||
return self.has("key", player) # or whatever
|
||||
```
|
||||
@@ -636,8 +654,8 @@ import .Logic # apply the mixin by importing its file
|
||||
class MyGameWorld(World):
|
||||
# ...
|
||||
def set_rules(self):
|
||||
set_rule(self.world.get_location("A Door", self.player),
|
||||
lamda state: state.mygame_has_key(self.player))
|
||||
set_rule(self.multiworld.get_location("A Door", self.player),
|
||||
lambda state: state.mygame_has_key(self.player))
|
||||
```
|
||||
|
||||
### Generate Output
|
||||
@@ -665,14 +683,14 @@ def generate_output(self, output_directory: str):
|
||||
# store option name "easy", "normal" or "hard" for difficuly
|
||||
"difficulty": self.multiworld.difficulty[self.player].current_key,
|
||||
# store option value True or False for fixing a glitch
|
||||
"fix_xyz_glitch": self.multiworld.fix_xyz_glitch[self.player].value
|
||||
"fix_xyz_glitch": self.multiworld.fix_xyz_glitch[self.player].value,
|
||||
}
|
||||
# point to a ROM specified by the installation
|
||||
src = Utils.get_options()["mygame_options"]["rom_file"]
|
||||
src = self.settings.rom_file
|
||||
# or point to worlds/mygame/data/mod_template
|
||||
src = os.path.join(os.path.dirname(__file__), "data", "mod_template")
|
||||
# generate output path
|
||||
mod_name = f"AP-{self.multiworld.seed_name}-P{self.player}-{self.multiworld.player_name[self.player]}"
|
||||
mod_name = self.multiworld.get_out_file_name_base(self.player)
|
||||
out_file = os.path.join(output_directory, mod_name + ".zip")
|
||||
# generate the file
|
||||
generate_mod(src, out_file, data)
|
||||
@@ -721,14 +739,14 @@ from . import MyGameTestBase
|
||||
|
||||
|
||||
class TestChestAccess(MyGameTestBase):
|
||||
def testSwordChests(self):
|
||||
def test_sword_chests(self):
|
||||
"""Test locations that require a sword"""
|
||||
locations = ["Chest1", "Chest2"]
|
||||
items = [["Sword"]]
|
||||
# this will test that each location can't be accessed without the "Sword", but can be accessed once obtained.
|
||||
self.assertAccessDependency(locations, items)
|
||||
|
||||
def testAnyWeaponChests(self):
|
||||
|
||||
def test_any_weapon_chests(self):
|
||||
"""Test locations that require any weapon"""
|
||||
locations = [f"Chest{i}" for i in range(3, 6)]
|
||||
items = [["Sword"], ["Axe"], ["Spear"]]
|
||||
|
||||
@@ -5,6 +5,7 @@ A world maintainer is a person responsible for a world or part of a world in Arc
|
||||
If a world author does not want to take on the responsibilities of a world maintainer, they can release their world as
|
||||
an unofficial [APWorld](/docs/apworld%20specification.md) or maintain their own fork instead.
|
||||
|
||||
All current world maintainers are listed in the [CODEOWNERS](/docs/CODEOWNERS) document.
|
||||
|
||||
## Responsibilities
|
||||
|
||||
@@ -18,15 +19,15 @@ Unless these are shared between multiple people, we expect the following from ea
|
||||
pull requests. Core maintainers may also ping you if a pull request concerns your world.
|
||||
* Test (or have tested) the world on the main branch from time to time, especially during RC (release candidate) phases
|
||||
of development.
|
||||
* Let us know of long unavailabilities.
|
||||
|
||||
* Let us know of long periods of unavailability.
|
||||
|
||||
## Becoming a World Maintainer
|
||||
|
||||
### Adding a World
|
||||
|
||||
When we merge your world into the core Archipelago repository, you automatically become world maintainer unless you
|
||||
nominate someone else (i.e. there are multiple devs).
|
||||
nominate someone else (i.e. there are multiple devs). You can define who is allowed to approve changes to your world
|
||||
in the [CODEOWNERS](/docs/CODEOWNERS) document.
|
||||
|
||||
### Getting Voted
|
||||
|
||||
@@ -36,12 +37,12 @@ For a vote to pass, the majority of participating core maintainers must vote in
|
||||
The time limit is 1 week, but can end early if the majority is reached earlier.
|
||||
Voting shall be conducted on Discord in #archipelago-dev.
|
||||
|
||||
|
||||
## Dropping out
|
||||
|
||||
### Resigning
|
||||
|
||||
A world maintainer can resign. If no new maintainer steps up and gets voted, the world becomes unmaintained.
|
||||
A world maintainer can resign and have their username removed from the [CODEOWNERS](/docs/CODEOWNERS) document. If no
|
||||
new maintainer takes over management of the world, the world becomes unmaintained.
|
||||
|
||||
### Getting Voted out
|
||||
|
||||
@@ -53,7 +54,6 @@ made their case or was pinged and has been unreachable for more than 2 weeks alr
|
||||
Voting shall be conducted on Discord in #archipelago-dev. Commits that are a direct result of the voting shall include
|
||||
date, voting members and final result in the commit message.
|
||||
|
||||
|
||||
## Handling of Unmaintained Worlds
|
||||
|
||||
As long as worlds are known to work for the most part, they can stay included. Once a world becomes broken it shall be
|
||||
|
||||
190
host.yaml
190
host.yaml
@@ -1,190 +0,0 @@
|
||||
general_options:
|
||||
# Where to place output files
|
||||
output_path: "output"
|
||||
# Options for MultiServer
|
||||
# Null means nothing, for the server this means to default the value
|
||||
# These overwrite command line arguments!
|
||||
server_options:
|
||||
host: null
|
||||
port: 38281
|
||||
password: null
|
||||
multidata: null
|
||||
savefile: null
|
||||
disable_save: false
|
||||
loglevel: "info"
|
||||
# Allows for clients to log on and manage the server. If this is null, no remote administration is possible.
|
||||
server_password: null
|
||||
# Disallow !getitem.
|
||||
disable_item_cheat: false
|
||||
# Client hint system
|
||||
# Points given to a player for each acquired item in their world
|
||||
location_check_points: 1
|
||||
# Relative point cost to receive a hint via !hint for players
|
||||
# so for example hint_cost: 20 would mean that for every 20% of available checks, you get the ability to hint, for a total of 5
|
||||
hint_cost: 10 # Set to 0 if you want free hints
|
||||
# Release modes
|
||||
# A Release sends out the remaining items *from* a world that releases
|
||||
# "disabled" -> clients can't release,
|
||||
# "enabled" -> clients can always release
|
||||
# "auto" -> automatic release on goal completion
|
||||
# "auto-enabled" -> automatic release on goal completion and manual release is also enabled
|
||||
# "goal" -> release is allowed after goal completion
|
||||
release_mode: "goal"
|
||||
# Collect modes
|
||||
# A Collect sends the remaining items *to* a world that collects
|
||||
# "disabled" -> clients can't collect,
|
||||
# "enabled" -> clients can always collect
|
||||
# "auto" -> automatic collect on goal completion
|
||||
# "auto-enabled" -> automatic collect on goal completion and manual collect is also enabled
|
||||
# "goal" -> collect is allowed after goal completion
|
||||
collect_mode: "goal"
|
||||
# Remaining modes
|
||||
# !remaining handling, that tells a client which items remain in their pool
|
||||
# "enabled" -> Client can always ask for remaining items
|
||||
# "disabled" -> Client can never ask for remaining items
|
||||
# "goal" -> Client can ask for remaining items after goal completion
|
||||
remaining_mode: "goal"
|
||||
# Automatically shut down the server after this many seconds without new location checks, 0 to keep running
|
||||
auto_shutdown: 0
|
||||
# Compatibility handling
|
||||
# 2 -> Recommended for casual/cooperative play, attempt to be compatible with everything across all versions
|
||||
# 1 -> No longer in use, kept reserved in case of future use
|
||||
# 0 -> Recommended for tournaments to force a level playing field, only allow an exact version match
|
||||
compatibility: 2
|
||||
# log all server traffic, mostly for dev use
|
||||
log_network: 0
|
||||
# Options for Generation
|
||||
generator:
|
||||
# Location of your Enemizer CLI, available here: https://github.com/Ijwu/Enemizer/releases
|
||||
enemizer_path: "EnemizerCLI/EnemizerCLI.Core" # + ".exe" is implied on Windows
|
||||
# Folder from which the player yaml files are pulled from
|
||||
player_files_path: "Players"
|
||||
#amount of players, 0 to infer from player files
|
||||
players: 0
|
||||
# general weights file, within the stated player_files_path location
|
||||
# gets used if players is higher than the amount of per-player files found to fill remaining slots
|
||||
weights_file_path: "weights.yaml"
|
||||
# Meta file name, within the stated player_files_path location
|
||||
meta_file_path: "meta.yaml"
|
||||
# Create a spoiler file
|
||||
# 0 -> None
|
||||
# 1 -> Spoiler without playthrough or paths to playthrough required items
|
||||
# 2 -> Spoiler with playthrough (viable solution to goals)
|
||||
# 3 -> Spoiler with playthrough and traversal paths towards items
|
||||
spoiler: 3
|
||||
# Glitch to Triforce room from Ganon
|
||||
# When disabled, you have to have a weapon that can hurt ganon (master sword or swordless/easy item functionality + hammer)
|
||||
# and have completed the goal required for killing ganon to be able to access the triforce room.
|
||||
# 1 -> Enabled.
|
||||
# 0 -> Disabled (except in no-logic)
|
||||
glitch_triforce_room: 1
|
||||
# Create encrypted race roms and flag games as race mode
|
||||
race: 0
|
||||
# List of options that can be plando'd. Can be combined, for example "bosses, items"
|
||||
# Available options: bosses, items, texts, connections
|
||||
plando_options: "bosses"
|
||||
sni_options:
|
||||
# Set this to your SNI folder location if you want the MultiClient to attempt an auto start, does nothing if not found
|
||||
sni_path: "SNI"
|
||||
# Set this to false to never autostart a rom (such as after patching)
|
||||
# True for operating system default program
|
||||
# Alternatively, a path to a program to open the .sfc file with
|
||||
snes_rom_start: true
|
||||
lttp_options:
|
||||
# File name of the v1.0 J rom
|
||||
rom_file: "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc"
|
||||
ladx_options:
|
||||
# File name of the Link's Awakening DX rom
|
||||
rom_file: "Legend of Zelda, The - Link's Awakening DX (USA, Europe) (SGB Enhanced).gbc"
|
||||
|
||||
lufia2ac_options:
|
||||
# File name of the US rom
|
||||
rom_file: "Lufia II - Rise of the Sinistrals (USA).sfc"
|
||||
sm_options:
|
||||
# File name of the v1.0 J rom
|
||||
rom_file: "Super Metroid (JU).sfc"
|
||||
factorio_options:
|
||||
executable: "factorio/bin/x64/factorio"
|
||||
# by default, no settings are loaded if this file does not exist. If this file does exist, then it will be used.
|
||||
# server_settings: "factorio\\data\\server-settings.json"
|
||||
# Whether to filter item send messages displayed in-game to only those that involve you.
|
||||
filter_item_sends: false
|
||||
# Whether to send chat messages from players on the Factorio server to Archipelago.
|
||||
bridge_chat_out: true
|
||||
minecraft_options:
|
||||
forge_directory: "Minecraft Forge server"
|
||||
max_heap_size: "2G"
|
||||
# release channel, currently "release", or "beta"
|
||||
# any games played on the "beta" channel have a high likelihood of no longer working on the "release" channel.
|
||||
release_channel: "release"
|
||||
oot_options:
|
||||
# File name of the OoT v1.0 ROM
|
||||
rom_file: "The Legend of Zelda - Ocarina of Time.z64"
|
||||
# Set this to false to never autostart a rom (such as after patching)
|
||||
# true for operating system default program
|
||||
# Alternatively, a path to a program to open the .z64 file with
|
||||
rom_start: true
|
||||
soe_options:
|
||||
# File name of the SoE US ROM
|
||||
rom_file: "Secret of Evermore (USA).sfc"
|
||||
ffr_options:
|
||||
display_msgs: true
|
||||
tloz_options:
|
||||
# File name of the Zelda 1
|
||||
rom_file: "Legend of Zelda, The (U) (PRG0) [!].nes"
|
||||
# Set this to false to never autostart a rom (such as after patching)
|
||||
# true for operating system default program
|
||||
# Alternatively, a path to a program to open the .nes file with
|
||||
rom_start: true
|
||||
# Display message inside of EmuHawk
|
||||
display_msgs: true
|
||||
dkc3_options:
|
||||
# File name of the DKC3 US rom
|
||||
rom_file: "Donkey Kong Country 3 - Dixie Kong's Double Trouble! (USA) (En,Fr).sfc"
|
||||
smw_options:
|
||||
# File name of the SMW US rom
|
||||
rom_file: "Super Mario World (USA).sfc"
|
||||
pokemon_rb_options:
|
||||
# File names of the Pokemon Red and Blue roms
|
||||
red_rom_file: "Pokemon Red (UE) [S][!].gb"
|
||||
blue_rom_file: "Pokemon Blue (UE) [S][!].gb"
|
||||
# Set this to false to never autostart a rom (such as after patching)
|
||||
# True for operating system default program
|
||||
# Alternatively, a path to a program to open the .gb file with
|
||||
rom_start: true
|
||||
|
||||
wargroove_options:
|
||||
# Locate the Wargroove root directory on your system.
|
||||
# This is used by the Wargroove client, so it knows where to send communication files to
|
||||
root_directory: "C:/Program Files (x86)/Steam/steamapps/common/Wargroove"
|
||||
|
||||
zillion_options:
|
||||
# File name of the Zillion US rom
|
||||
rom_file: "Zillion (UE) [!].sms"
|
||||
# Set this to false to never autostart a rom (such as after patching)
|
||||
# True for operating system default program
|
||||
# Alternatively, a path to a program to open the .sfc file with
|
||||
# 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"
|
||||
mmbn3_options:
|
||||
# File name of the MMBN3 Blue US rom
|
||||
rom_file: "Mega Man Battle Network 3 - Blue Version (USA).gba"
|
||||
rom_start: true
|
||||
adventure_options:
|
||||
# File name of the standard NTSC Adventure rom.
|
||||
# The licensed "The 80 Classic Games" CD-ROM contains this.
|
||||
# It may also have a .a26 extension
|
||||
rom_file: "ADVNTURE.BIN"
|
||||
# Set this to false to never autostart a rom (such as after patching)
|
||||
# True for operating system default program for '.a26'
|
||||
# Alternatively, a path to a program to open the .a26 file with (generally EmuHawk for multiworld)
|
||||
rom_start: true
|
||||
# Optional, additional args passed into rom_start before the .bin file
|
||||
# For example, this can be used to autoload the connector script in EmuHawk
|
||||
# (see EmuHawk --lua= option)
|
||||
# Windows example:
|
||||
# rom_args: "--lua=C:/ProgramData/Archipelago/data/lua/connector_adventure.lua"
|
||||
rom_args: " "
|
||||
# Set this to true to display item received messages in Emuhawk
|
||||
display_msgs: true
|
||||
@@ -2,10 +2,10 @@
|
||||
#define min_windows ReadIni(SourcePath + "\setup.ini", "Data", "min_windows")
|
||||
|
||||
#define MyAppName "Archipelago"
|
||||
#define MyAppExeName "ArchipelagoServer.exe"
|
||||
#define MyAppExeName "ArchipelagoLauncher.exe"
|
||||
#define MyAppIcon "data/icon.ico"
|
||||
#dim VersionTuple[4]
|
||||
#define MyAppVersion GetVersionComponents(source_path + '\ArchipelagoServer.exe', VersionTuple[0], VersionTuple[1], VersionTuple[2], VersionTuple[3])
|
||||
#define MyAppVersion GetVersionComponents(source_path + '\ArchipelagoLauncher.exe', VersionTuple[0], VersionTuple[1], VersionTuple[2], VersionTuple[3])
|
||||
#define MyAppVersionText Str(VersionTuple[0])+"."+Str(VersionTuple[1])+"."+Str(VersionTuple[2])
|
||||
|
||||
|
||||
@@ -141,7 +141,8 @@ Source: "vc_redist.x64.exe"; DestDir: {tmp}; Flags: deleteafterinstall
|
||||
|
||||
[Icons]
|
||||
Name: "{group}\{#MyAppName} Folder"; Filename: "{app}";
|
||||
Name: "{group}\{#MyAppName} Server"; Filename: "{app}\{#MyAppExeName}"; Components: server
|
||||
Name: "{group}\{#MyAppName} Launcher"; Filename: "{app}\ArchipelagoLauncher.exe"
|
||||
Name: "{group}\{#MyAppName} Server"; Filename: "{app}\ArchipelagoServer"; Components: server
|
||||
Name: "{group}\{#MyAppName} Text Client"; Filename: "{app}\ArchipelagoTextClient.exe"; Components: client/text
|
||||
Name: "{group}\{#MyAppName} SNI Client"; Filename: "{app}\ArchipelagoSNIClient.exe"; Components: client/sni
|
||||
Name: "{group}\{#MyAppName} Factorio Client"; Filename: "{app}\ArchipelagoFactorioClient.exe"; Components: client/factorio
|
||||
@@ -155,12 +156,14 @@ Name: "{group}\{#MyAppName} Starcraft 2 Client"; Filename: "{app}\ArchipelagoSta
|
||||
Name: "{group}\{#MyAppName} MegaMan Battle Network 3 Client"; Filename: "{app}\ArchipelagoMMBN3Client.exe"; Components: client/mmbn3
|
||||
Name: "{group}\{#MyAppName} The Legend of Zelda Client"; Filename: "{app}\ArchipelagoZelda1Client.exe"; Components: client/tloz
|
||||
Name: "{group}\{#MyAppName} Kingdom Hearts 2 Client"; Filename: "{app}\ArchipelagoKH2Client.exe"; Components: client/kh2
|
||||
Name: "{group}\{#MyAppName} Link's Awakening Client"; Filename: "{app}\ArchipelagoLinksAwakeningClient.exe"; Components: client/ladx
|
||||
Name: "{group}\{#MyAppName} Adventure Client"; Filename: "{app}\ArchipelagoAdventureClient.exe"; Components: client/advn
|
||||
Name: "{group}\{#MyAppName} Wargroove Client"; Filename: "{app}\ArchipelagoWargrooveClient.exe"; Components: client/wargroove
|
||||
Name: "{group}\{#MyAppName} Undertale Client"; Filename: "{app}\ArchipelagoUndertaleClient.exe"; Components: client/ut
|
||||
|
||||
Name: "{commondesktop}\{#MyAppName} Folder"; Filename: "{app}"; Tasks: desktopicon
|
||||
Name: "{commondesktop}\{#MyAppName} Server"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon; Components: server
|
||||
Name: "{commondesktop}\{#MyAppName} Launcher"; Filename: "{app}\ArchipelagoLauncher.exe"; Tasks: desktopicon
|
||||
Name: "{commondesktop}\{#MyAppName} Server"; Filename: "{app}\ArchipelagoServer"; Tasks: desktopicon; Components: server
|
||||
Name: "{commondesktop}\{#MyAppName} SNI Client"; Filename: "{app}\ArchipelagoSNIClient.exe"; Tasks: desktopicon; Components: client/sni
|
||||
Name: "{commondesktop}\{#MyAppName} Factorio Client"; Filename: "{app}\ArchipelagoFactorioClient.exe"; Tasks: desktopicon; Components: client/factorio
|
||||
Name: "{commondesktop}\{#MyAppName} Minecraft Client"; Filename: "{app}\ArchipelagoMinecraftClient.exe"; Tasks: desktopicon; Components: client/minecraft
|
||||
@@ -174,6 +177,7 @@ Name: "{commondesktop}\{#MyAppName} MegaMan Battle Network 3 Client"; Filename:
|
||||
Name: "{commondesktop}\{#MyAppName} The Legend of Zelda Client"; Filename: "{app}\ArchipelagoZelda1Client.exe"; Tasks: desktopicon; Components: client/tloz
|
||||
Name: "{commondesktop}\{#MyAppName} Wargroove Client"; Filename: "{app}\ArchipelagoWargrooveClient.exe"; Tasks: desktopicon; Components: client/wargroove
|
||||
Name: "{commondesktop}\{#MyAppName} Kingdom Hearts 2 Client"; Filename: "{app}\ArchipelagoKH2Client.exe"; Tasks: desktopicon; Components: client/kh2
|
||||
Name: "{commondesktop}\{#MyAppName} Link's Awakening Client"; Filename: "{app}\ArchipelagoLinksAwakeningClient.exe"; Tasks: desktopicon; Components: client/ladx
|
||||
Name: "{commondesktop}\{#MyAppName} Adventure Client"; Filename: "{app}\ArchipelagoAdventureClient.exe"; Tasks: desktopicon; Components: client/advn
|
||||
Name: "{commondesktop}\{#MyAppName} Undertale Client"; Filename: "{app}\ArchipelagoUndertaleClient.exe"; Tasks: desktopicon; Components: client/ut
|
||||
|
||||
@@ -182,6 +186,8 @@ Name: "{commondesktop}\{#MyAppName} Undertale Client"; Filename: "{app}\Archipel
|
||||
Filename: "{tmp}\vc_redist.x64.exe"; Parameters: "/passive /norestart"; Check: IsVCRedist64BitNeeded; StatusMsg: "Installing VC++ redistributable..."
|
||||
Filename: "{app}\ArchipelagoLttPAdjuster"; Parameters: "--update_sprites"; StatusMsg: "Updating Sprite Library..."; Components: client/sni/lttp or generator/lttp
|
||||
Filename: "{app}\ArchipelagoMinecraftClient.exe"; Parameters: "--install"; StatusMsg: "Installing Forge Server..."; Components: client/minecraft
|
||||
Filename: "{app}\ArchipelagoLauncher"; Parameters: "--update_settings"; StatusMsg: "Updating host.yaml..."; Flags: runasoriginaluser runhidden
|
||||
Filename: "{app}\ArchipelagoLauncher"; Description: "{cm:LaunchProgram,{#StringChange('Launcher', '&', '&&')}}"; Flags: nowait postinstall skipifsilent
|
||||
|
||||
[UninstallDelete]
|
||||
Type: dirifempty; Name: "{app}"
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
colorama>=0.4.5
|
||||
websockets>=11.0.3
|
||||
PyYAML>=6.0
|
||||
jellyfish>=0.11.2
|
||||
PyYAML>=6.0.1
|
||||
jellyfish>=1.0.0
|
||||
jinja2>=3.1.2
|
||||
schema>=0.7.5
|
||||
kivy>=2.2.0
|
||||
bsdiff4>=1.2.3
|
||||
platformdirs>=3.5.1
|
||||
certifi>=2023.5.7
|
||||
platformdirs>=3.9.1
|
||||
certifi>=2023.7.22
|
||||
cython>=0.29.35
|
||||
cymem>=2.0.7
|
||||
|
||||
836
settings.py
Normal file
836
settings.py
Normal file
@@ -0,0 +1,836 @@
|
||||
"""
|
||||
Application settings / host.yaml interface using type hints.
|
||||
This is different from player settings.
|
||||
"""
|
||||
|
||||
import os.path
|
||||
import shutil
|
||||
import sys
|
||||
import typing
|
||||
import warnings
|
||||
from enum import IntEnum
|
||||
from threading import Lock
|
||||
from typing import cast, Any, BinaryIO, ClassVar, Dict, Iterator, List, Optional, TextIO, Tuple, Union, TypeVar
|
||||
import os
|
||||
|
||||
__all__ = [
|
||||
"get_settings", "fmt_doc", "no_gui",
|
||||
"Group", "Bool", "Path", "UserFilePath", "UserFolderPath", "LocalFilePath", "LocalFolderPath",
|
||||
"OptionalUserFilePath", "OptionalUserFolderPath", "OptionalLocalFilePath", "OptionalLocalFolderPath",
|
||||
"GeneralOptions", "ServerOptions", "GeneratorOptions", "SNIOptions", "Settings"
|
||||
]
|
||||
|
||||
no_gui = False
|
||||
skip_autosave = False
|
||||
_world_settings_name_cache: Dict[str, str] = {} # TODO: cache on disk and update when worlds change
|
||||
_world_settings_name_cache_updated = False
|
||||
_lock = Lock()
|
||||
|
||||
|
||||
def _update_cache() -> None:
|
||||
"""Load all worlds and update world_settings_name_cache"""
|
||||
global _world_settings_name_cache_updated
|
||||
if _world_settings_name_cache_updated:
|
||||
return
|
||||
|
||||
try:
|
||||
from worlds.AutoWorld import AutoWorldRegister
|
||||
for world in AutoWorldRegister.world_types.values():
|
||||
annotation = world.__annotations__.get("settings", None)
|
||||
if annotation is None or annotation == "ClassVar[Optional['Group']]":
|
||||
continue
|
||||
_world_settings_name_cache[world.settings_key] = f"{world.__module__}.{world.__name__}"
|
||||
finally:
|
||||
_world_settings_name_cache_updated = True
|
||||
|
||||
|
||||
def fmt_doc(cls: type, level: int) -> str:
|
||||
comment = cls.__doc__
|
||||
assert comment, f"{cls} has no __doc__"
|
||||
indent = level * 2 * " "
|
||||
return "\n".join(map(lambda s: f"{indent}# {s}", filter(None, map(lambda s: s.strip(), comment.split("\n")))))
|
||||
|
||||
|
||||
class Group:
|
||||
_type_cache: ClassVar[Optional[Dict[str, Any]]] = None
|
||||
_dumping: bool = False
|
||||
_has_attr: bool = False
|
||||
_changed: bool = False
|
||||
_dumper: ClassVar[type]
|
||||
|
||||
def __getitem__(self, key: str) -> Any:
|
||||
try:
|
||||
return getattr(self, key)
|
||||
except NameError:
|
||||
raise KeyError(key)
|
||||
|
||||
def __iter__(self) -> Iterator[str]:
|
||||
cls_members = dir(self.__class__)
|
||||
members = filter(lambda k: not k.startswith("_") and (k not in cls_members or k in self.__annotations__),
|
||||
list(self.__annotations__) +
|
||||
[name for name in dir(self) if name not in self.__annotations__])
|
||||
return members.__iter__()
|
||||
|
||||
def __contains__(self, key: str) -> bool:
|
||||
try:
|
||||
self._has_attr = True
|
||||
return hasattr(self, key)
|
||||
finally:
|
||||
self._has_attr = False
|
||||
|
||||
def __setitem__(self, key: str, value: Any) -> None:
|
||||
setattr(self, key, value)
|
||||
|
||||
def __getattribute__(self, item: str) -> Any:
|
||||
attr = super().__getattribute__(item)
|
||||
if isinstance(attr, Path) and not super().__getattribute__("_dumping"):
|
||||
if attr.required and not attr.exists() and not super().__getattribute__("_has_attr"):
|
||||
# if a file is required, and the one from settings does not exist, ask the user to provide it
|
||||
# unless we are dumping the settings, because that would ask for each entry
|
||||
with _lock: # lock to avoid opening multiple
|
||||
new = None if no_gui else attr.browse()
|
||||
if new is None:
|
||||
raise FileNotFoundError(f"{attr} does not exist, but "
|
||||
f"{self.__class__.__name__}.{item} is required")
|
||||
setattr(self, item, new)
|
||||
self._changed = True
|
||||
attr = new
|
||||
# resolve the path immediately when accessing it
|
||||
return attr.__class__(attr.resolve())
|
||||
return attr
|
||||
|
||||
@property
|
||||
def changed(self) -> bool:
|
||||
return self._changed or any(map(lambda v: isinstance(v, Group) and v.changed,
|
||||
self.__dict__.values()))
|
||||
|
||||
@classmethod
|
||||
def get_type_hints(cls) -> Dict[str, Any]:
|
||||
"""Returns resolved type hints for the class"""
|
||||
if cls._type_cache is None:
|
||||
if not isinstance(next(iter(cls.__annotations__.values())), str):
|
||||
# non-str: assume already resolved
|
||||
cls._type_cache = cls.__annotations__
|
||||
else:
|
||||
# str: build dicts and resolve with eval
|
||||
mod = sys.modules[cls.__module__] # assume the module wasn't deleted
|
||||
mod_dict = {k: getattr(mod, k) for k in dir(mod)}
|
||||
cls._type_cache = typing.get_type_hints(cls, globalns=mod_dict, localns=cls.__dict__)
|
||||
return cls._type_cache
|
||||
|
||||
def get(self, key: str, default: Any) -> Any:
|
||||
if key in self:
|
||||
return self[key]
|
||||
return default
|
||||
|
||||
def items(self) -> List[Tuple[str, Any]]:
|
||||
return [(key, getattr(self, key)) for key in self]
|
||||
|
||||
def update(self, dct: Dict[str, Any]) -> None:
|
||||
assert isinstance(dct, dict), f"{self.__class__.__name__}.update called with " \
|
||||
f"{dct.__class__.__name__} instead of dict."
|
||||
|
||||
for k in self.__annotations__:
|
||||
if not k.startswith("_") and k not in dct:
|
||||
self._changed = True # key missing from host.yaml
|
||||
|
||||
for k, v in dct.items():
|
||||
# don't do getattr to stay lazy with world group init/loading
|
||||
# instead we assign unknown groups as dicts and a later getattr will upcast them
|
||||
attr = self.__dict__[k] if k in self.__dict__ else \
|
||||
self.__class__.__dict__[k] if k in self.__class__.__dict__ else None
|
||||
if isinstance(attr, Group):
|
||||
# update group
|
||||
if k not in self.__dict__:
|
||||
attr = attr.__class__() # make a copy of default
|
||||
setattr(self, k, attr)
|
||||
if isinstance(v, dict):
|
||||
attr.update(v)
|
||||
else:
|
||||
warnings.warn(f"{self.__class__.__name__}.{k} "
|
||||
f"tried to update Group from {type(v)}")
|
||||
elif isinstance(attr, dict):
|
||||
# update dict
|
||||
if k not in self.__dict__:
|
||||
attr = attr.copy() # make a copy of default
|
||||
setattr(self, k, attr)
|
||||
if isinstance(v, dict):
|
||||
attr.update(v)
|
||||
else:
|
||||
warnings.warn(f"{self.__class__.__name__}.{k} "
|
||||
f"tried to update dict from {type(v)}")
|
||||
else:
|
||||
# assign value, try to upcast to type hint
|
||||
annotation = self.get_type_hints().get(k, None)
|
||||
candidates = [] if annotation is None else \
|
||||
typing.get_args(annotation) if typing.get_origin(annotation) is Union else [annotation]
|
||||
none_type = type(None)
|
||||
for cls in candidates:
|
||||
assert isinstance(cls, type), f"{self.__class__.__name__}.{k}: type {cls} not supported in settings"
|
||||
if v is None and cls is none_type:
|
||||
# assign None, i.e. from Optional
|
||||
setattr(self, k, v)
|
||||
break
|
||||
if cls is bool and isinstance(v, bool):
|
||||
# assign bool - special handling because issubclass(int, bool) is True
|
||||
setattr(self, k, v)
|
||||
break
|
||||
if cls is not bool and issubclass(cls, type(v)):
|
||||
# upcast, i.e. int -> IntEnum, str -> Path
|
||||
setattr(self, k, cls.__call__(v))
|
||||
break
|
||||
if issubclass(cls, (tuple, set)) and isinstance(v, list):
|
||||
# convert or upcast from list
|
||||
setattr(self, k, cls.__call__(v))
|
||||
break
|
||||
else:
|
||||
# assign scalar and hope for the best
|
||||
setattr(self, k, v)
|
||||
if annotation:
|
||||
warnings.warn(f"{self.__class__.__name__}.{k} "
|
||||
f"assigned from incompatible type {type(v).__name__}")
|
||||
|
||||
def as_dict(self, *args: str, downcast: bool = True) -> Dict[str, Any]:
|
||||
return {
|
||||
name: _to_builtin(cast(object, getattr(self, name))) if downcast else getattr(self, name)
|
||||
for name in self if not args or name in args
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def _dump_value(cls, value: Any, f: TextIO, indent: str) -> None:
|
||||
"""Write a single yaml line to f"""
|
||||
from Utils import dump, Dumper as BaseDumper
|
||||
yaml_line: str = dump(value, Dumper=cast(BaseDumper, cls._dumper))
|
||||
assert yaml_line.count("\n") == 1, f"Unexpected input for yaml dumper: {value}"
|
||||
f.write(f"{indent}{yaml_line}")
|
||||
|
||||
@classmethod
|
||||
def _dump_item(cls, name: Optional[str], attr: object, f: TextIO, level: int) -> None:
|
||||
"""Write a group, dict or sequence item to f, where attr can be a scalar or a collection"""
|
||||
|
||||
# lazy construction of yaml Dumper to avoid loading Utils early
|
||||
from Utils import Dumper as BaseDumper
|
||||
from yaml import ScalarNode, MappingNode
|
||||
if not hasattr(cls, "_dumper"):
|
||||
if cls is Group or not hasattr(Group, "_dumper"):
|
||||
class Dumper(BaseDumper):
|
||||
def represent_mapping(self, tag: str, mapping: Any, flow_style: Any = None) -> MappingNode:
|
||||
from yaml import ScalarNode
|
||||
res: MappingNode = super().represent_mapping(tag, mapping, flow_style)
|
||||
pairs = cast(List[Tuple[ScalarNode, Any]], res.value)
|
||||
for k, v in pairs:
|
||||
k.style = None # remove quotes from keys
|
||||
return res
|
||||
|
||||
def represent_str(self, data: str) -> ScalarNode:
|
||||
# default double quote all strings
|
||||
return self.represent_scalar("tag:yaml.org,2002:str", data, style='"')
|
||||
|
||||
Dumper.add_representer(str, Dumper.represent_str)
|
||||
Group._dumper = Dumper
|
||||
if cls is not Group:
|
||||
cls._dumper = Group._dumper
|
||||
|
||||
indent = " " * level
|
||||
start = f"{indent}-\n" if name is None else f"{indent}{name}:\n"
|
||||
if isinstance(attr, Group):
|
||||
# handle group
|
||||
f.write(start)
|
||||
attr.dump(f, level=level+1)
|
||||
elif isinstance(attr, (list, tuple, set)) and attr:
|
||||
# handle non-empty sequence; empty use one-line [] syntax
|
||||
f.write(start)
|
||||
for value in attr:
|
||||
cls._dump_item(None, value, f, level=level + 1)
|
||||
elif isinstance(attr, dict) and attr:
|
||||
# handle non-empty dict; empty use one-line {} syntax
|
||||
f.write(start)
|
||||
for dict_key, value in attr.items():
|
||||
# not dumping doc string here, since there is no way to upcast it after dumping
|
||||
assert dict_key is not None, "Key None is reserved for sequences"
|
||||
cls._dump_item(dict_key, value, f, level=level + 1)
|
||||
else:
|
||||
# dump scalar or empty sequence or mapping item
|
||||
line = [_to_builtin(attr)] if name is None else {name: _to_builtin(attr)}
|
||||
cls._dump_value(line, f, indent=indent)
|
||||
|
||||
def dump(self, f: TextIO, level: int = 0) -> None:
|
||||
"""Dump Group to stream f at given indentation level"""
|
||||
# There is no easy way to generate extra lines into default yaml output,
|
||||
# so we format part of it by hand using an odd recursion here and in _dump_*.
|
||||
|
||||
self._dumping = True
|
||||
try:
|
||||
# fetch class to avoid going through getattr
|
||||
cls = self.__class__
|
||||
type_hints = cls.get_type_hints()
|
||||
# validate group
|
||||
for name in cls.__annotations__.keys():
|
||||
assert hasattr(cls, name), f"{cls}.{name} is missing a default value"
|
||||
# dump ordered members
|
||||
for name in self:
|
||||
attr = cast(object, getattr(self, name))
|
||||
attr_cls = type_hints[name] if name in type_hints else attr.__class__
|
||||
attr_cls_origin = typing.get_origin(attr_cls)
|
||||
while attr_cls_origin is Union: # resolve to first type for doc string
|
||||
attr_cls = typing.get_args(attr_cls)[0]
|
||||
attr_cls_origin = typing.get_origin(attr_cls)
|
||||
if attr_cls.__doc__ and attr_cls.__module__ != "builtins":
|
||||
f.write(fmt_doc(attr_cls, level=level) + "\n")
|
||||
self._dump_item(name, attr, f, level=level)
|
||||
self._changed = False
|
||||
finally:
|
||||
self._dumping = False
|
||||
|
||||
|
||||
class Bool:
|
||||
# can't subclass bool, so we use this and Union or type: ignore
|
||||
def __bool__(self) -> bool:
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
# Types for generic settings
|
||||
T = TypeVar("T", bound="Path")
|
||||
|
||||
|
||||
def _resolve_exe(s: str) -> str:
|
||||
"""Append exe file extension if the file is an executable"""
|
||||
if isinstance(s, Path):
|
||||
from Utils import is_windows
|
||||
if s.is_exe and is_windows and not s.lower().endswith(".exe"):
|
||||
return str(s + ".exe")
|
||||
return str(s)
|
||||
|
||||
|
||||
def _to_builtin(o: object) -> Any:
|
||||
"""Downcast object to a builtin type for output"""
|
||||
if o is None:
|
||||
return None
|
||||
c = o.__class__
|
||||
while c.__module__ != "builtins":
|
||||
c = c.__base__
|
||||
return c.__call__(o)
|
||||
|
||||
|
||||
class Path(str):
|
||||
# paths in host.yaml are str
|
||||
required: bool = True
|
||||
"""Marks the file as required and opens a file browser when missing"""
|
||||
is_exe: bool = False
|
||||
"""Special cross-platform handling for executables"""
|
||||
description: Optional[str] = None
|
||||
"""Title to display when browsing for the file"""
|
||||
copy_to: Optional[str] = None
|
||||
"""If not None, copy to AP folder instead of linking it"""
|
||||
|
||||
@classmethod
|
||||
def validate(cls, path: str) -> None:
|
||||
"""Overload and raise to validate input files from browse"""
|
||||
pass
|
||||
|
||||
def browse(self: T, **kwargs: Any) -> Optional[T]:
|
||||
"""Opens a file browser to search for the file"""
|
||||
raise NotImplementedError(f"Please use a subclass of Path for {self.__class__.__name__}")
|
||||
|
||||
def resolve(self) -> str:
|
||||
return _resolve_exe(self)
|
||||
|
||||
def exists(self) -> bool:
|
||||
return os.path.exists(self.resolve())
|
||||
|
||||
|
||||
class _UserPath(str):
|
||||
def resolve(self) -> str:
|
||||
if os.path.isabs(self):
|
||||
return str(self)
|
||||
from Utils import user_path
|
||||
return user_path(_resolve_exe(self))
|
||||
|
||||
|
||||
class _LocalPath(str):
|
||||
def resolve(self) -> str:
|
||||
if os.path.isabs(self):
|
||||
return str(self)
|
||||
from Utils import local_path
|
||||
return local_path(_resolve_exe(self))
|
||||
|
||||
|
||||
class FilePath(Path):
|
||||
# path to a file
|
||||
|
||||
md5s: ClassVar[List[Union[str, bytes]]] = []
|
||||
"""MD5 hashes for default validator."""
|
||||
|
||||
def browse(self: T,
|
||||
filetypes: Optional[typing.Sequence[typing.Tuple[str, typing.Sequence[str]]]] = None, **kwargs: Any)\
|
||||
-> Optional[T]:
|
||||
from Utils import open_filename, is_windows
|
||||
if not filetypes:
|
||||
if self.is_exe:
|
||||
name, ext = "Program", ".exe" if is_windows else ""
|
||||
else:
|
||||
ext = os.path.splitext(self)[1]
|
||||
name = ext[1:] if ext else "File"
|
||||
filetypes = [(name, [ext])]
|
||||
res = open_filename(f"Select {self.description or self.__class__.__name__}", filetypes, self)
|
||||
if res:
|
||||
self.validate(res)
|
||||
if self.copy_to:
|
||||
# instead of linking the file, copy it
|
||||
dst = self.__class__(self.copy_to).resolve()
|
||||
shutil.copy(res, dst, follow_symlinks=True)
|
||||
res = dst
|
||||
try:
|
||||
rel = os.path.relpath(res, self.__class__("").resolve())
|
||||
if not rel.startswith(".."):
|
||||
res = rel
|
||||
except ValueError:
|
||||
pass
|
||||
return self.__class__(res)
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def _validate_stream_hashes(cls, f: BinaryIO) -> None:
|
||||
"""Helper to efficiently validate stream against hashes"""
|
||||
if not cls.md5s:
|
||||
return # no hashes to validate against
|
||||
|
||||
pos = f.tell()
|
||||
try:
|
||||
from hashlib import md5
|
||||
file_md5 = md5()
|
||||
block = bytearray(64*1024)
|
||||
view = memoryview(block)
|
||||
while n := f.readinto(view): # type: ignore
|
||||
file_md5.update(view[:n])
|
||||
file_md5_hex = file_md5.hexdigest()
|
||||
for valid_md5 in cls.md5s:
|
||||
if isinstance(valid_md5, str):
|
||||
if valid_md5.lower() == file_md5_hex:
|
||||
break
|
||||
elif valid_md5 == file_md5.digest():
|
||||
break
|
||||
else:
|
||||
raise ValueError(f"Hashes do not match for {cls.__name__}")
|
||||
finally:
|
||||
f.seek(pos)
|
||||
|
||||
@classmethod
|
||||
def validate(cls, path: str) -> None:
|
||||
"""Try to open and validate file against hashes"""
|
||||
with open(path, "rb", buffering=0) as f:
|
||||
try:
|
||||
cls._validate_stream_hashes(f)
|
||||
except ValueError:
|
||||
raise ValueError(f"File hash does not match for {path}")
|
||||
|
||||
|
||||
class FolderPath(Path):
|
||||
# path to a folder
|
||||
|
||||
def browse(self: T, **kwargs: Any) -> Optional[T]:
|
||||
from Utils import open_directory
|
||||
res = open_directory(f"Select {self.description or self.__class__.__name__}", self)
|
||||
if res:
|
||||
try:
|
||||
rel = os.path.relpath(res, self.__class__("").resolve())
|
||||
if not rel.startswith(".."):
|
||||
res = rel
|
||||
except ValueError:
|
||||
pass
|
||||
return self.__class__(res)
|
||||
return None
|
||||
|
||||
|
||||
class UserFilePath(_UserPath, FilePath):
|
||||
pass
|
||||
|
||||
|
||||
class UserFolderPath(_UserPath, FolderPath):
|
||||
pass
|
||||
|
||||
|
||||
class OptionalUserFilePath(UserFilePath):
|
||||
required = False
|
||||
|
||||
|
||||
class OptionalUserFolderPath(UserFolderPath):
|
||||
required = False
|
||||
|
||||
|
||||
class LocalFilePath(_LocalPath, FilePath):
|
||||
pass
|
||||
|
||||
|
||||
class LocalFolderPath(_LocalPath, FolderPath):
|
||||
pass
|
||||
|
||||
|
||||
class OptionalLocalFilePath(LocalFilePath):
|
||||
required = False
|
||||
|
||||
|
||||
class OptionalLocalFolderPath(LocalFolderPath):
|
||||
required = False
|
||||
|
||||
|
||||
class SNESRomPath(UserFilePath):
|
||||
# Special UserFilePath that ignores an optional header when validating
|
||||
|
||||
@classmethod
|
||||
def validate(cls, path: str) -> None:
|
||||
"""Try to open and validate file against hashes"""
|
||||
with open(path, "rb", buffering=0) as f:
|
||||
f.seek(0, os.SEEK_END)
|
||||
size = f.tell()
|
||||
if size % 1024 == 512:
|
||||
f.seek(512) # skip header
|
||||
elif size % 1024 == 0:
|
||||
f.seek(0) # header-less
|
||||
else:
|
||||
raise ValueError(f"Unexpected file size for {path}")
|
||||
|
||||
try:
|
||||
cls._validate_stream_hashes(f)
|
||||
except ValueError:
|
||||
raise ValueError(f"File hash does not match for {path}")
|
||||
|
||||
|
||||
# World-independent setting groups
|
||||
|
||||
class GeneralOptions(Group):
|
||||
class OutputPath(OptionalUserFolderPath):
|
||||
"""
|
||||
Where to place output files
|
||||
"""
|
||||
# created on demand, so marked as optional
|
||||
|
||||
output_path: OutputPath = OutputPath("output")
|
||||
|
||||
|
||||
class ServerOptions(Group):
|
||||
"""
|
||||
Options for MultiServer
|
||||
Null means nothing, for the server this means to default the value
|
||||
These overwrite command line arguments!
|
||||
"""
|
||||
|
||||
class ServerPassword(str):
|
||||
"""
|
||||
Allows for clients to log on and manage the server. If this is null, no remote administration is possible.
|
||||
"""
|
||||
|
||||
class DisableItemCheat(Bool):
|
||||
"""Disallow !getitem"""
|
||||
|
||||
class LocationCheckPoints(int):
|
||||
"""
|
||||
Client hint system
|
||||
Points given to a player for each acquired item in their world
|
||||
"""
|
||||
|
||||
class HintCost(int):
|
||||
"""
|
||||
Relative point cost to receive a hint via !hint for players
|
||||
so for example hint_cost: 20 would mean that for every 20% of available checks, you get the ability to hint,
|
||||
for a total of 5
|
||||
"""
|
||||
|
||||
class ReleaseMode(str):
|
||||
"""
|
||||
Release modes
|
||||
A Release sends out the remaining items *from* a world that releases
|
||||
"disabled" -> clients can't release,
|
||||
"enabled" -> clients can always release
|
||||
"auto" -> automatic release on goal completion
|
||||
"auto-enabled" -> automatic release on goal completion and manual release is also enabled
|
||||
"goal" -> release is allowed after goal completion
|
||||
"""
|
||||
|
||||
class CollectMode(str):
|
||||
"""
|
||||
Collect modes
|
||||
A Collect sends the remaining items *to* a world that collects
|
||||
"disabled" -> clients can't collect,
|
||||
"enabled" -> clients can always collect
|
||||
"auto" -> automatic collect on goal completion
|
||||
"auto-enabled" -> automatic collect on goal completion and manual collect is also enabled
|
||||
"goal" -> collect is allowed after goal completion
|
||||
"""
|
||||
|
||||
class RemainingMode(str):
|
||||
"""
|
||||
Remaining modes
|
||||
!remaining handling, that tells a client which items remain in their pool
|
||||
"enabled" -> Client can always ask for remaining items
|
||||
"disabled" -> Client can never ask for remaining items
|
||||
"goal" -> Client can ask for remaining items after goal completion
|
||||
"""
|
||||
|
||||
class AutoShutdown(int):
|
||||
"""Automatically shut down the server after this many seconds without new location checks, 0 to keep running"""
|
||||
|
||||
class Compatibility(IntEnum):
|
||||
"""
|
||||
Compatibility handling
|
||||
2 -> Recommended for casual/cooperative play, attempt to be compatible with everything across all versions
|
||||
1 -> No longer in use, kept reserved in case of future use
|
||||
0 -> Recommended for tournaments to force a level playing field, only allow an exact version match
|
||||
"""
|
||||
OFF = 0
|
||||
ON = 1
|
||||
FULL = 2
|
||||
|
||||
class LogNetwork(IntEnum):
|
||||
"""log all server traffic, mostly for dev use"""
|
||||
OFF = 0
|
||||
ON = 1
|
||||
|
||||
host: Optional[str] = None
|
||||
port: int = 38281
|
||||
password: Optional[str] = None
|
||||
multidata: Optional[str] = None
|
||||
savefile: Optional[str] = None
|
||||
disable_save: bool = False
|
||||
loglevel: str = "info"
|
||||
server_password: Optional[ServerPassword] = None
|
||||
disable_item_cheat: Union[DisableItemCheat, bool] = False
|
||||
location_check_points: LocationCheckPoints = LocationCheckPoints(1)
|
||||
hint_cost: HintCost = HintCost(10)
|
||||
release_mode: ReleaseMode = ReleaseMode("goal")
|
||||
collect_mode: CollectMode = CollectMode("goal")
|
||||
remaining_mode: RemainingMode = RemainingMode("goal")
|
||||
auto_shutdown: AutoShutdown = AutoShutdown(0)
|
||||
compatibility: Compatibility = Compatibility(2)
|
||||
log_network: LogNetwork = LogNetwork(0)
|
||||
|
||||
|
||||
class GeneratorOptions(Group):
|
||||
"""Options for Generation"""
|
||||
|
||||
class EnemizerPath(LocalFilePath):
|
||||
"""Location of your Enemizer CLI, available here: https://github.com/Ijwu/Enemizer/releases"""
|
||||
is_exe = True
|
||||
|
||||
class PlayerFilesPath(OptionalUserFolderPath):
|
||||
"""Folder from which the player yaml files are pulled from"""
|
||||
# created on demand, so marked as optional
|
||||
|
||||
class Players(int):
|
||||
"""amount of players, 0 to infer from player files"""
|
||||
|
||||
class WeightsFilePath(str):
|
||||
"""
|
||||
general weights file, within the stated player_files_path location
|
||||
gets used if players is higher than the amount of per-player files found to fill remaining slots
|
||||
"""
|
||||
# this is special because the path is relative to player_files_path
|
||||
|
||||
class MetaFilePath(str):
|
||||
"""Meta file name, within the stated player_files_path location"""
|
||||
# this is special because the path is relative to player_files_path
|
||||
|
||||
class Spoiler(IntEnum):
|
||||
"""
|
||||
Create a spoiler file
|
||||
0 -> None
|
||||
1 -> Spoiler without playthrough or paths to playthrough required items
|
||||
2 -> Spoiler with playthrough (viable solution to goals)
|
||||
3 -> Spoiler with playthrough and traversal paths towards items
|
||||
"""
|
||||
NONE = 0
|
||||
BASIC = 1
|
||||
PLAYTHROUGH = 2
|
||||
FULL = 3
|
||||
|
||||
class GlitchTriforceRoom(IntEnum):
|
||||
"""
|
||||
Glitch to Triforce room from Ganon
|
||||
When disabled, you have to have a weapon that can hurt ganon (master sword or swordless/easy item functionality
|
||||
+ hammer) and have completed the goal required for killing ganon to be able to access the triforce room.
|
||||
1 -> Enabled.
|
||||
0 -> Disabled (except in no-logic)
|
||||
"""
|
||||
OFF = 0
|
||||
ON = 1
|
||||
|
||||
class PlandoOptions(str):
|
||||
"""
|
||||
List of options that can be plando'd. Can be combined, for example "bosses, items"
|
||||
Available options: bosses, items, texts, connections
|
||||
"""
|
||||
|
||||
class Race(IntEnum):
|
||||
"""Create encrypted race roms and flag games as race mode"""
|
||||
OFF = 0
|
||||
ON = 1
|
||||
|
||||
enemizer_path: EnemizerPath = EnemizerPath("EnemizerCLI/EnemizerCLI.Core") # + ".exe" is implied on Windows
|
||||
player_files_path: PlayerFilesPath = PlayerFilesPath("Players")
|
||||
players: Players = Players(0)
|
||||
weights_file_path: WeightsFilePath = WeightsFilePath("weights.yaml")
|
||||
meta_file_path: MetaFilePath = MetaFilePath("meta.yaml")
|
||||
spoiler: Spoiler = Spoiler(3)
|
||||
glitch_triforce_room: GlitchTriforceRoom = GlitchTriforceRoom(1) # why is this here?
|
||||
race: Race = Race(0)
|
||||
plando_options: PlandoOptions = PlandoOptions("bosses")
|
||||
|
||||
|
||||
class SNIOptions(Group):
|
||||
class SNIPath(LocalFolderPath):
|
||||
"""
|
||||
Set this to your SNI folder location if you want the MultiClient to attempt an auto start, \
|
||||
does nothing if not found
|
||||
"""
|
||||
|
||||
class SnesRomStart(str):
|
||||
"""
|
||||
Set this to false to never autostart a rom (such as after patching)
|
||||
True for operating system default program
|
||||
Alternatively, a path to a program to open the .sfc file with
|
||||
"""
|
||||
|
||||
sni_path: SNIPath = SNIPath("SNI")
|
||||
snes_rom_start: Union[SnesRomStart, bool] = True
|
||||
|
||||
|
||||
# Top-level group with lazy loading of worlds
|
||||
|
||||
class Settings(Group):
|
||||
general_options: GeneralOptions = GeneralOptions()
|
||||
server_options: ServerOptions = ServerOptions()
|
||||
generator: GeneratorOptions = GeneratorOptions()
|
||||
sni_options: SNIOptions = SNIOptions()
|
||||
|
||||
_filename: Optional[str] = None
|
||||
|
||||
def __getattribute__(self, key: str) -> Any:
|
||||
if key.startswith("_") or key in self.__class__.__dict__:
|
||||
# not a group or a hard-coded group
|
||||
pass
|
||||
elif key not in dir(self) or isinstance(super().__getattribute__(key), dict):
|
||||
# settings class not loaded yet
|
||||
if key not in _world_settings_name_cache:
|
||||
# find world that provides the settings class
|
||||
_update_cache()
|
||||
# check for missing keys to update _changed
|
||||
for world_settings_name in _world_settings_name_cache:
|
||||
if world_settings_name not in dir(self):
|
||||
self._changed = True
|
||||
if key not in _world_settings_name_cache:
|
||||
# not a world group
|
||||
return super().__getattribute__(key)
|
||||
# directly import world and grab settings class
|
||||
world_mod, world_cls_name = _world_settings_name_cache[key].rsplit(".", 1)
|
||||
world = cast(type, getattr(__import__(world_mod, fromlist=[world_cls_name]), world_cls_name))
|
||||
assert getattr(world, "settings_key") == key
|
||||
try:
|
||||
cls_or_name = world.__annotations__["settings"]
|
||||
except KeyError:
|
||||
import warnings
|
||||
warnings.warn(f"World {world_cls_name} does not define settings. Please consider upgrading the world.")
|
||||
return super().__getattribute__(key)
|
||||
if isinstance(cls_or_name, str):
|
||||
# Try to resolve type. Sadly we can't use get_type_hints, see https://bugs.python.org/issue43463
|
||||
cls_name = cls_or_name
|
||||
if "[" in cls_name: # resolve ClassVar[]
|
||||
cls_name = cls_name.split("[", 1)[1].rsplit("]", 1)[0]
|
||||
cls = cast(type, getattr(__import__(world_mod, fromlist=[cls_name]), cls_name))
|
||||
else:
|
||||
type_args = typing.get_args(cls_or_name) # resolve ClassVar[]
|
||||
cls = type_args[0] if type_args else cast(type, cls_or_name)
|
||||
impl: Group = cast(Group, cls())
|
||||
assert isinstance(impl, Group), f"{world_cls_name}.settings has to inherit from settings.Group. " \
|
||||
"If that's already the case, please avoid recursive partial imports."
|
||||
# above assert fails for recursive partial imports
|
||||
# upcast loaded data to settings class
|
||||
try:
|
||||
dct = super().__getattribute__(key)
|
||||
if isinstance(dct, dict):
|
||||
impl.update(dct)
|
||||
else:
|
||||
self._changed = True # key is a class var -> new section
|
||||
except AttributeError:
|
||||
self._changed = True # key is unknown -> new section
|
||||
setattr(self, key, impl)
|
||||
|
||||
return super().__getattribute__(key)
|
||||
|
||||
def __init__(self, location: Optional[str]): # change to PathLike[str] once we drop 3.8?
|
||||
super().__init__()
|
||||
if location:
|
||||
from Utils import parse_yaml
|
||||
with open(location, encoding="utf-8-sig") as f:
|
||||
options = parse_yaml(f.read())
|
||||
# TODO: detect if upgrade is required
|
||||
# TODO: once we have a cache for _world_settings_name_cache, detect if any game section is missing
|
||||
self.update(options or {})
|
||||
self._filename = location
|
||||
|
||||
def autosave() -> None:
|
||||
if __debug__:
|
||||
import __main__
|
||||
main_file = getattr(__main__, "__file__", "")
|
||||
assert "pytest" not in main_file and "unittest" not in main_file, \
|
||||
f"Auto-saving {self._filename} during unittests"
|
||||
if self._filename and self.changed and not skip_autosave:
|
||||
self.save()
|
||||
|
||||
if not skip_autosave:
|
||||
import atexit
|
||||
atexit.register(autosave)
|
||||
|
||||
def save(self, location: Optional[str] = None) -> None: # as above
|
||||
location = location or self._filename
|
||||
assert location, "No file specified"
|
||||
temp_location = location + ".tmp" # not using tempfile to test expected file access
|
||||
# remove old temps
|
||||
if os.path.exists(temp_location):
|
||||
os.unlink(temp_location)
|
||||
# can't use utf-8-sig because it breaks backward compat: pyyaml on Windows with bytes does not strip the BOM
|
||||
with open(temp_location, "w", encoding="utf-8") as f:
|
||||
self.dump(f)
|
||||
# replace old with new
|
||||
if os.path.exists(location):
|
||||
os.unlink(location)
|
||||
os.rename(temp_location, location)
|
||||
self._filename = location
|
||||
|
||||
def dump(self, f: TextIO, level: int = 0) -> None:
|
||||
# load all world setting classes
|
||||
_update_cache()
|
||||
for key in _world_settings_name_cache:
|
||||
self.__getattribute__(key) # load all worlds
|
||||
super().dump(f, level)
|
||||
|
||||
@property
|
||||
def filename(self) -> Optional[str]:
|
||||
return self._filename
|
||||
|
||||
|
||||
# host.yaml loader
|
||||
|
||||
def get_settings() -> Settings:
|
||||
"""Returns settings from the default host.yaml"""
|
||||
with _lock: # make sure we only have one instance
|
||||
res = getattr(get_settings, "_cache", None)
|
||||
if not res:
|
||||
import os
|
||||
from Utils import user_path, local_path
|
||||
filenames = ("options.yaml", "host.yaml")
|
||||
locations: 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:
|
||||
try:
|
||||
res = Settings(location)
|
||||
break
|
||||
except FileNotFoundError:
|
||||
continue
|
||||
else:
|
||||
warnings.warn(f"Could not find {filenames[1]} to load options. Creating a new one.")
|
||||
res = Settings(None)
|
||||
res.save(user_path(filenames[1]))
|
||||
setattr(get_settings, "_cache", res)
|
||||
return res
|
||||
48
setup.py
48
setup.py
@@ -6,6 +6,7 @@ import shutil
|
||||
import sys
|
||||
import sysconfig
|
||||
import typing
|
||||
import warnings
|
||||
import zipfile
|
||||
import urllib.request
|
||||
import io
|
||||
@@ -20,7 +21,7 @@ from pathlib import Path
|
||||
|
||||
# This is a bit jank. We need cx-Freeze to be able to run anything from this script, so install it
|
||||
try:
|
||||
requirement = 'cx-Freeze==6.14.9'
|
||||
requirement = 'cx-Freeze>=6.15.2'
|
||||
import pkg_resources
|
||||
try:
|
||||
pkg_resources.require(requirement)
|
||||
@@ -57,6 +58,7 @@ if __name__ == "__main__":
|
||||
|
||||
from worlds.LauncherComponents import components, icon_paths
|
||||
from Utils import version_tuple, is_windows, is_linux
|
||||
from Cython.Build import cythonize
|
||||
|
||||
|
||||
# On Python < 3.10 LogicMixin is not currently supported.
|
||||
@@ -65,20 +67,16 @@ non_apworlds: set = {
|
||||
"Adventure",
|
||||
"ArchipIDLE",
|
||||
"Archipelago",
|
||||
"Blasphemous",
|
||||
"ChecksFinder",
|
||||
"Clique",
|
||||
"DLCQuest",
|
||||
"Dark Souls III",
|
||||
"Final Fantasy",
|
||||
"Hollow Knight",
|
||||
"Hylics 2",
|
||||
"Kingdom Hearts 2",
|
||||
"Lufia II Ancient Cave",
|
||||
"Meritous",
|
||||
"Ocarina of Time",
|
||||
"Overcooked! 2",
|
||||
"Pokemon Red and Blue",
|
||||
"Raft",
|
||||
"Secret of Evermore",
|
||||
"Slay the Spire",
|
||||
@@ -90,6 +88,9 @@ non_apworlds: set = {
|
||||
"Zillion",
|
||||
}
|
||||
|
||||
# LogicMixin is broken before 3.10 import revamp
|
||||
if sys.version_info < (3,10):
|
||||
non_apworlds.add("Hollow Knight")
|
||||
|
||||
def download_SNI():
|
||||
print("Updating SNI")
|
||||
@@ -191,7 +192,7 @@ exes = [
|
||||
) for c in components if c.script_name and c.frozen_name
|
||||
]
|
||||
|
||||
extra_data = ["LICENSE", "data", "EnemizerCLI", "host.yaml", "SNI"]
|
||||
extra_data = ["LICENSE", "data", "EnemizerCLI", "SNI"]
|
||||
extra_libs = ["libssl.so", "libcrypto.so"] if is_linux else []
|
||||
|
||||
|
||||
@@ -293,17 +294,38 @@ class BuildExeCommand(cx_Freeze.command.build_exe.BuildEXE):
|
||||
sni_thread = threading.Thread(target=download_SNI, name="SNI Downloader")
|
||||
sni_thread.start()
|
||||
|
||||
# pre build steps
|
||||
# pre-build steps
|
||||
print(f"Outputting to: {self.buildfolder}")
|
||||
os.makedirs(self.buildfolder, exist_ok=True)
|
||||
import ModuleUpdate
|
||||
ModuleUpdate.requirements_files.add(os.path.join("WebHostLib", "requirements.txt"))
|
||||
ModuleUpdate.update(yes=self.yes)
|
||||
|
||||
# auto-build cython modules
|
||||
build_ext = self.distribution.get_command_obj("build_ext")
|
||||
build_ext.inplace = False
|
||||
self.run_command("build_ext")
|
||||
# find remains of previous in-place builds, try to delete and warn otherwise
|
||||
for path in build_ext.get_outputs():
|
||||
parts = os.path.split(path)[-1].split(".")
|
||||
pattern = parts[0] + ".*." + parts[-1]
|
||||
for match in Path().glob(pattern):
|
||||
try:
|
||||
match.unlink()
|
||||
print(f"Removed {match}")
|
||||
except Exception as ex:
|
||||
warnings.warn(f"Could not delete old build output: {match}\n"
|
||||
f"{ex}\nPlease close all AP instances and delete manually.")
|
||||
|
||||
# regular cx build
|
||||
self.buildtime = datetime.datetime.utcnow()
|
||||
super().run()
|
||||
|
||||
# manually copy built modules to lib folder. cx_Freeze does not know they exist.
|
||||
for src in build_ext.get_outputs():
|
||||
print(f"copying {src} -> {self.libfolder}")
|
||||
shutil.copy(src, self.libfolder, follow_symlinks=False)
|
||||
|
||||
# need to finish download before copying
|
||||
sni_thread.join()
|
||||
|
||||
@@ -396,14 +418,6 @@ class BuildExeCommand(cx_Freeze.command.build_exe.BuildEXE):
|
||||
for extra_exe in extra_exes:
|
||||
if extra_exe.is_file():
|
||||
extra_exe.chmod(0o755)
|
||||
# rewrite windows-specific things in host.yaml
|
||||
host_yaml = self.buildfolder / 'host.yaml'
|
||||
with host_yaml.open('r+b') as f:
|
||||
data = f.read()
|
||||
data = data.replace(b'factorio\\\\bin\\\\x64\\\\factorio', b'factorio/bin/x64/factorio')
|
||||
f.seek(0, os.SEEK_SET)
|
||||
f.write(data)
|
||||
f.truncate()
|
||||
|
||||
|
||||
class AppImageCommand(setuptools.Command):
|
||||
@@ -586,10 +600,10 @@ cx_Freeze.setup(
|
||||
version=f"{version_tuple.major}.{version_tuple.minor}.{version_tuple.build}",
|
||||
description="Archipelago",
|
||||
executables=exes,
|
||||
ext_modules=[], # required to disable auto-discovery with setuptools>=61
|
||||
ext_modules=cythonize("_speedups.pyx"),
|
||||
options={
|
||||
"build_exe": {
|
||||
"packages": ["websockets", "worlds", "kivy"],
|
||||
"packages": ["worlds", "kivy", "cymem", "websockets"],
|
||||
"includes": [],
|
||||
"excludes": ["numpy", "Cython", "PySide2", "PIL",
|
||||
"pandas"],
|
||||
|
||||
@@ -237,7 +237,8 @@ class WorldTestBase(unittest.TestCase):
|
||||
for location in self.multiworld.get_locations():
|
||||
if location.name not in excluded:
|
||||
with self.subTest("Location should be reached", location=location):
|
||||
self.assertTrue(location.can_reach(state), f"{location.name} unreachable")
|
||||
reachable = location.can_reach(state)
|
||||
self.assertTrue(reachable, f"{location.name} unreachable")
|
||||
with self.subTest("Beatable"):
|
||||
self.multiworld.state = state
|
||||
self.assertBeatable(True)
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
import warnings
|
||||
warnings.simplefilter("always")
|
||||
|
||||
import settings
|
||||
|
||||
warnings.simplefilter("always")
|
||||
settings.no_gui = True
|
||||
settings.skip_autosave = True
|
||||
|
||||
@@ -433,6 +433,20 @@ class TestFillRestrictive(unittest.TestCase):
|
||||
self.assertTrue(multi_world.state.prog_items[item.name, item.player], "Sweep did not collect - Test flawed")
|
||||
self.assertEqual(multi_world.state.prog_items[item.name, item.player], 1, "Sweep collected multiple times")
|
||||
|
||||
def test_correct_item_instance_removed_from_pool(self):
|
||||
multi_world = generate_multi_world()
|
||||
player1 = generate_player_data(multi_world, 1, 2, 2)
|
||||
|
||||
player1.prog_items[0].name = "Different_item_instance_but_same_item_name"
|
||||
player1.prog_items[1].name = "Different_item_instance_but_same_item_name"
|
||||
loc0 = player1.locations[0]
|
||||
|
||||
fill_restrictive(multi_world, multi_world.state,
|
||||
[loc0], player1.prog_items)
|
||||
|
||||
self.assertEqual(1, len(player1.prog_items))
|
||||
self.assertIsNot(loc0.item, player1.prog_items[0], "Filled item was still present in item pool")
|
||||
|
||||
|
||||
class TestDistributeItemsRestrictive(unittest.TestCase):
|
||||
def test_basic_distribute(self):
|
||||
|
||||
@@ -19,6 +19,7 @@ class TestHelpers(unittest.TestCase):
|
||||
regions: Dict[str, str] = {
|
||||
"TestRegion1": "I'm an apple",
|
||||
"TestRegion2": "I'm a banana",
|
||||
"TestRegion3": "Empty Region",
|
||||
}
|
||||
|
||||
locations: Dict[str, Dict[str, Optional[int]]] = {
|
||||
@@ -38,6 +39,10 @@ class TestHelpers(unittest.TestCase):
|
||||
"TestRegion2": {"TestRegion1": None},
|
||||
}
|
||||
|
||||
reg_exit_set: Dict[str, set[str]] = {
|
||||
"TestRegion1": {"TestRegion3"}
|
||||
}
|
||||
|
||||
exit_rules: Dict[str, Callable[[CollectionState], bool]] = {
|
||||
"TestRegion1": lambda state: state.has("test_item", self.player)
|
||||
}
|
||||
@@ -68,3 +73,10 @@ class TestHelpers(unittest.TestCase):
|
||||
entrance_name = exit_name if exit_name else f"{parent} -> {exit_reg}"
|
||||
self.assertEqual(exit_rules[exit_reg],
|
||||
self.multiworld.get_entrance(entrance_name, self.player).access_rule)
|
||||
|
||||
for region in reg_exit_set:
|
||||
current_region = self.multiworld.get_region(region, self.player)
|
||||
current_region.add_exits(reg_exit_set[region])
|
||||
exit_names = {_exit.name for _exit in current_region.exits}
|
||||
for reg_exit in reg_exit_set[region]:
|
||||
self.assertTrue(f"{region} -> {reg_exit}" in exit_names, f"{region} -> {reg_exit} not in {exit_names}")
|
||||
|
||||
@@ -1,22 +1,27 @@
|
||||
import os
|
||||
import unittest
|
||||
from tempfile import TemporaryFile
|
||||
|
||||
from settings import Settings
|
||||
import Utils
|
||||
|
||||
|
||||
class TestIDs(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls) -> None:
|
||||
with open(Utils.local_path("host.yaml")) as f:
|
||||
with TemporaryFile("w+", encoding="utf-8") as f:
|
||||
Settings(None).dump(f)
|
||||
f.seek(0, os.SEEK_SET)
|
||||
cls.yaml_options = Utils.parse_yaml(f.read())
|
||||
|
||||
def testUtilsHasHost(self):
|
||||
def test_utils_in_yaml(self) -> None:
|
||||
for option_key, option_set in Utils.get_default_options().items():
|
||||
with self.subTest(option_key):
|
||||
self.assertIn(option_key, self.yaml_options)
|
||||
for sub_option_key in option_set:
|
||||
self.assertIn(sub_option_key, self.yaml_options[option_key])
|
||||
|
||||
def testHostHasUtils(self):
|
||||
def test_yaml_in_utils(self) -> None:
|
||||
utils_options = Utils.get_default_options()
|
||||
for option_key, option_set in self.yaml_options.items():
|
||||
with self.subTest(option_key):
|
||||
|
||||
@@ -44,7 +44,10 @@ class TestBase(unittest.TestCase):
|
||||
self.assertTrue(location.can_reach(state), f"{location.name} unreachable")
|
||||
|
||||
for region in world.get_regions():
|
||||
if region.name not in unreachable_regions:
|
||||
if region.name in unreachable_regions:
|
||||
with self.subTest("Region should be unreachable", region=region):
|
||||
self.assertFalse(region.can_reach(state))
|
||||
else:
|
||||
with self.subTest("Region should be reached", region=region):
|
||||
self.assertTrue(region.can_reach(state))
|
||||
|
||||
|
||||
238
test/netutils/TestLocationStore.py
Normal file
238
test/netutils/TestLocationStore.py
Normal file
@@ -0,0 +1,238 @@
|
||||
# Tests for _speedups.LocationStore and NetUtils._LocationStore
|
||||
import typing
|
||||
import unittest
|
||||
import warnings
|
||||
from NetUtils import LocationStore, _LocationStore
|
||||
|
||||
State = typing.Dict[typing.Tuple[int, int], typing.Set[int]]
|
||||
RawLocations = typing.Dict[int, typing.Dict[int, typing.Tuple[int, int, int]]]
|
||||
|
||||
sample_data: RawLocations = {
|
||||
1: {
|
||||
11: (21, 2, 7),
|
||||
12: (22, 2, 0),
|
||||
13: (13, 1, 0),
|
||||
},
|
||||
2: {
|
||||
23: (11, 1, 0),
|
||||
22: (12, 1, 0),
|
||||
21: (23, 2, 0),
|
||||
},
|
||||
4: {
|
||||
9: (99, 3, 0),
|
||||
},
|
||||
3: {
|
||||
9: (99, 4, 0),
|
||||
},
|
||||
}
|
||||
|
||||
empty_state: State = {
|
||||
(0, slot): set() for slot in sample_data
|
||||
}
|
||||
|
||||
full_state: State = {
|
||||
(0, slot): set(locations) for (slot, locations) in sample_data.items()
|
||||
}
|
||||
|
||||
one_state: State = {
|
||||
(0, 1): {12}
|
||||
}
|
||||
|
||||
|
||||
class Base:
|
||||
class TestLocationStore(unittest.TestCase):
|
||||
"""Test method calls on a loaded store."""
|
||||
store: typing.Union[LocationStore, _LocationStore]
|
||||
|
||||
def test_len(self) -> None:
|
||||
self.assertEqual(len(self.store), 4)
|
||||
self.assertEqual(len(self.store[1]), 3)
|
||||
|
||||
def test_key_error(self) -> None:
|
||||
with self.assertRaises(KeyError):
|
||||
_ = self.store[0]
|
||||
with self.assertRaises(KeyError):
|
||||
_ = self.store[5]
|
||||
locations = self.store[1] # no Exception
|
||||
with self.assertRaises(KeyError):
|
||||
_ = locations[7]
|
||||
_ = locations[11] # no Exception
|
||||
|
||||
def test_getitem(self) -> None:
|
||||
self.assertEqual(self.store[1][11], (21, 2, 7))
|
||||
self.assertEqual(self.store[1][13], (13, 1, 0))
|
||||
self.assertEqual(self.store[2][22], (12, 1, 0))
|
||||
self.assertEqual(self.store[4][9], (99, 3, 0))
|
||||
|
||||
def test_get(self) -> None:
|
||||
self.assertEqual(self.store.get(1, None), self.store[1])
|
||||
self.assertEqual(self.store.get(0, None), None)
|
||||
self.assertEqual(self.store[1].get(11, (None, None, None)), self.store[1][11])
|
||||
self.assertEqual(self.store[1].get(10, (None, None, None)), (None, None, None))
|
||||
|
||||
def test_iter(self) -> None:
|
||||
self.assertEqual(sorted(self.store), [1, 2, 3, 4])
|
||||
self.assertEqual(len(self.store), len(sample_data))
|
||||
self.assertEqual(list(self.store[1]), [11, 12, 13])
|
||||
self.assertEqual(len(self.store[1]), len(sample_data[1]))
|
||||
|
||||
def test_items(self) -> None:
|
||||
self.assertEqual(sorted(p for p, _ in self.store.items()), sorted(self.store))
|
||||
self.assertEqual(sorted(p for p, _ in self.store[1].items()), sorted(self.store[1]))
|
||||
self.assertEqual(sorted(self.store.items())[0][0], 1)
|
||||
self.assertEqual(sorted(self.store.items())[0][1], self.store[1])
|
||||
self.assertEqual(sorted(self.store[1].items())[0][0], 11)
|
||||
self.assertEqual(sorted(self.store[1].items())[0][1], self.store[1][11])
|
||||
|
||||
def test_find_item(self) -> None:
|
||||
self.assertEqual(sorted(self.store.find_item(set(), 99)), [])
|
||||
self.assertEqual(sorted(self.store.find_item({3}, 1)), [])
|
||||
self.assertEqual(sorted(self.store.find_item({5}, 99)), [])
|
||||
self.assertEqual(sorted(self.store.find_item({3}, 99)),
|
||||
[(4, 9, 99, 3, 0)])
|
||||
self.assertEqual(sorted(self.store.find_item({3, 4}, 99)),
|
||||
[(3, 9, 99, 4, 0), (4, 9, 99, 3, 0)])
|
||||
|
||||
def test_get_for_player(self) -> None:
|
||||
self.assertEqual(self.store.get_for_player(3), {4: {9}})
|
||||
self.assertEqual(self.store.get_for_player(1), {1: {13}, 2: {22, 23}})
|
||||
|
||||
def test_get_checked(self) -> None:
|
||||
self.assertEqual(self.store.get_checked(full_state, 0, 1), [11, 12, 13])
|
||||
self.assertEqual(self.store.get_checked(one_state, 0, 1), [12])
|
||||
self.assertEqual(self.store.get_checked(empty_state, 0, 1), [])
|
||||
self.assertEqual(self.store.get_checked(full_state, 0, 3), [9])
|
||||
|
||||
def test_get_missing(self) -> None:
|
||||
self.assertEqual(self.store.get_missing(full_state, 0, 1), [])
|
||||
self.assertEqual(self.store.get_missing(one_state, 0, 1), [11, 13])
|
||||
self.assertEqual(self.store.get_missing(empty_state, 0, 1), [11, 12, 13])
|
||||
self.assertEqual(self.store.get_missing(empty_state, 0, 3), [9])
|
||||
|
||||
def test_get_remaining(self) -> None:
|
||||
self.assertEqual(self.store.get_remaining(full_state, 0, 1), [])
|
||||
self.assertEqual(self.store.get_remaining(one_state, 0, 1), [13, 21])
|
||||
self.assertEqual(self.store.get_remaining(empty_state, 0, 1), [13, 21, 22])
|
||||
self.assertEqual(self.store.get_remaining(empty_state, 0, 3), [99])
|
||||
|
||||
def test_location_set_intersection(self) -> None:
|
||||
locations = {10, 11, 12}
|
||||
locations.intersection_update(self.store[1])
|
||||
self.assertEqual(locations, {11, 12})
|
||||
|
||||
class TestLocationStoreConstructor(unittest.TestCase):
|
||||
"""Test constructors for a given store type."""
|
||||
type: type
|
||||
|
||||
def test_hole(self) -> None:
|
||||
with self.assertRaises(Exception):
|
||||
self.type({
|
||||
1: {1: (1, 1, 1)},
|
||||
3: {1: (1, 1, 1)},
|
||||
})
|
||||
|
||||
def test_no_slot1(self) -> None:
|
||||
with self.assertRaises(Exception):
|
||||
self.type({
|
||||
2: {1: (1, 1, 1)},
|
||||
3: {1: (1, 1, 1)},
|
||||
})
|
||||
|
||||
def test_slot0(self) -> None:
|
||||
with self.assertRaises(ValueError):
|
||||
self.type({
|
||||
0: {1: (1, 1, 1)},
|
||||
1: {1: (1, 1, 1)},
|
||||
})
|
||||
with self.assertRaises(ValueError):
|
||||
self.type({
|
||||
0: {1: (1, 1, 1)},
|
||||
2: {1: (1, 1, 1)},
|
||||
})
|
||||
|
||||
def test_no_players(self) -> None:
|
||||
with self.assertRaises(Exception):
|
||||
_ = self.type({})
|
||||
|
||||
def test_no_locations(self) -> None:
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore")
|
||||
store = self.type({
|
||||
1: {},
|
||||
})
|
||||
self.assertEqual(len(store), 1)
|
||||
self.assertEqual(len(store[1]), 0)
|
||||
|
||||
def test_no_locations_for_1(self) -> None:
|
||||
store = self.type({
|
||||
1: {},
|
||||
2: {1: (1, 2, 3)},
|
||||
})
|
||||
self.assertEqual(len(store), 2)
|
||||
self.assertEqual(len(store[1]), 0)
|
||||
self.assertEqual(len(store[2]), 1)
|
||||
|
||||
def test_no_locations_for_last(self) -> None:
|
||||
store = self.type({
|
||||
1: {1: (1, 2, 3)},
|
||||
2: {},
|
||||
})
|
||||
self.assertEqual(len(store), 2)
|
||||
self.assertEqual(len(store[1]), 1)
|
||||
self.assertEqual(len(store[2]), 0)
|
||||
|
||||
|
||||
class TestPurePythonLocationStore(Base.TestLocationStore):
|
||||
"""Run base method tests for pure python implementation."""
|
||||
def setUp(self) -> None:
|
||||
self.store = _LocationStore(sample_data)
|
||||
super().setUp()
|
||||
|
||||
|
||||
class TestPurePythonLocationStoreConstructor(Base.TestLocationStoreConstructor):
|
||||
"""Run base constructor tests for the pure python implementation."""
|
||||
def setUp(self) -> None:
|
||||
self.type = _LocationStore
|
||||
super().setUp()
|
||||
|
||||
|
||||
@unittest.skipIf(LocationStore is _LocationStore, "_speedups not available")
|
||||
class TestSpeedupsLocationStore(Base.TestLocationStore):
|
||||
"""Run base method tests for cython implementation."""
|
||||
def setUp(self) -> None:
|
||||
self.store = LocationStore(sample_data)
|
||||
super().setUp()
|
||||
|
||||
|
||||
@unittest.skipIf(LocationStore is _LocationStore, "_speedups not available")
|
||||
class TestSpeedupsLocationStoreConstructor(Base.TestLocationStoreConstructor):
|
||||
"""Run base constructor tests and tests the additional constraints for cython implementation."""
|
||||
def setUp(self) -> None:
|
||||
self.type = LocationStore
|
||||
super().setUp()
|
||||
|
||||
def test_float_key(self) -> None:
|
||||
with self.assertRaises(Exception):
|
||||
self.type({
|
||||
1: {1: (1, 1, 1)},
|
||||
1.1: {1: (1, 1, 1)},
|
||||
3: {1: (1, 1, 1)}
|
||||
})
|
||||
|
||||
def test_string_key(self) -> None:
|
||||
with self.assertRaises(Exception):
|
||||
self.type({
|
||||
"1": {1: (1, 1, 1)},
|
||||
})
|
||||
|
||||
def test_high_player_number(self) -> None:
|
||||
with self.assertRaises(Exception):
|
||||
self.type({
|
||||
1 << 32: {1: (1, 1, 1)},
|
||||
})
|
||||
|
||||
def test_not_a_tuple(self) -> None:
|
||||
with self.assertRaises(Exception):
|
||||
self.type({
|
||||
1: {1: None},
|
||||
})
|
||||
0
test/netutils/__init__.py
Normal file
0
test/netutils/__init__.py
Normal file
@@ -73,13 +73,21 @@ class TestGenerateMain(unittest.TestCase):
|
||||
|
||||
def test_generate_yaml(self):
|
||||
# override host.yaml
|
||||
defaults = Generate.Utils.get_options()["generator"]
|
||||
defaults["player_files_path"] = str(self.yaml_input_dir)
|
||||
defaults["players"] = 0
|
||||
|
||||
sys.argv = [sys.argv[0], '--seed', '0',
|
||||
'--outputpath', self.output_tempdir.name]
|
||||
print(f'Testing Generate.py {sys.argv} in {os.getcwd()}, player_files_path={self.yaml_input_dir}')
|
||||
Generate.main()
|
||||
from settings import get_settings
|
||||
from Utils import user_path, local_path
|
||||
settings = get_settings()
|
||||
# NOTE: until/unless we override settings.Group's setattr, we have to upcast the input dir here
|
||||
settings.generator.player_files_path = settings.generator.PlayerFilesPath(self.yaml_input_dir)
|
||||
settings.generator.players = 0
|
||||
settings._filename = None # don't write to disk
|
||||
user_path_backup = user_path.cached_path
|
||||
user_path.cached_path = local_path() # test yaml is actually in local_path
|
||||
try:
|
||||
sys.argv = [sys.argv[0], '--seed', '0',
|
||||
'--outputpath', self.output_tempdir.name]
|
||||
print(f'Testing Generate.py {sys.argv} in {os.getcwd()}, player_files_path={self.yaml_input_dir}')
|
||||
Generate.main()
|
||||
finally:
|
||||
user_path.cached_path = user_path_backup
|
||||
|
||||
self.assertOutput(self.output_tempdir.name)
|
||||
|
||||
@@ -11,14 +11,27 @@ from BaseClasses import CollectionState
|
||||
from Options import AssembleOptions
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import random
|
||||
from BaseClasses import MultiWorld, Item, Location, Tutorial
|
||||
from . import GamesPackage
|
||||
from settings import Group
|
||||
from flask import Flask
|
||||
|
||||
|
||||
class AutoWorldRegister(type):
|
||||
world_types: Dict[str, Type[World]] = {}
|
||||
__file__: str
|
||||
zip_path: Optional[str]
|
||||
settings_key: str
|
||||
__settings: Any
|
||||
|
||||
@property
|
||||
def settings(cls) -> Any: # actual type is defined in World
|
||||
# lazy loading + caching to minimize runtime cost
|
||||
if cls.__settings is None:
|
||||
from settings import get_settings
|
||||
cls.__settings = get_settings()[cls.settings_key]
|
||||
return cls.__settings
|
||||
|
||||
def __new__(mcs, name: str, bases: Tuple[type, ...], dct: Dict[str, Any]) -> AutoWorldRegister:
|
||||
if "web" in dct:
|
||||
@@ -60,6 +73,11 @@ class AutoWorldRegister(type):
|
||||
new_class.__file__ = sys.modules[new_class.__module__].__file__
|
||||
if ".apworld" in new_class.__file__:
|
||||
new_class.zip_path = pathlib.Path(new_class.__file__).parents[1]
|
||||
if "settings_key" not in dct:
|
||||
mod_name = new_class.__module__
|
||||
world_folder_name = mod_name[7:].lower() if mod_name.startswith("worlds.") else mod_name.lower()
|
||||
new_class.settings_key = world_folder_name + "_options"
|
||||
new_class.__settings = None
|
||||
return new_class
|
||||
|
||||
|
||||
@@ -81,7 +99,17 @@ class AutoLogicRegister(type):
|
||||
|
||||
def call_single(multiworld: "MultiWorld", method_name: str, player: int, *args: Any) -> Any:
|
||||
method = getattr(multiworld.worlds[player], method_name)
|
||||
return method(*args)
|
||||
try:
|
||||
ret = method(*args)
|
||||
except Exception as e:
|
||||
message = f"Exception in {method} for player {player}, named {multiworld.player_name[player]}."
|
||||
if sys.version_info >= (3, 11, 0):
|
||||
e.add_note(message) # PEP 678
|
||||
else:
|
||||
logging.error(message)
|
||||
raise e
|
||||
else:
|
||||
return ret
|
||||
|
||||
|
||||
def call_all(multiworld: "MultiWorld", method_name: str, *args: Any) -> None:
|
||||
@@ -128,9 +156,22 @@ class WebWorld:
|
||||
"""Choose a theme for you /game/* pages.
|
||||
Available: dirt, grass, grassFlowers, ice, jungle, ocean, partyTime, stone"""
|
||||
|
||||
bug_report_page: Optional[str]
|
||||
bug_report_page: Optional[str] = None
|
||||
"""display a link to a bug report page, most likely a link to a GitHub issue page."""
|
||||
|
||||
multitracker_template: Optional[str] = None
|
||||
"""relative path with /-seperator to a MultiTracker Template file."""
|
||||
|
||||
# allows modification of webhost during startup, this is run once
|
||||
@classmethod
|
||||
def run_webhost_setup(cls):
|
||||
pass
|
||||
|
||||
# allows modification of webhost during startup,
|
||||
# this is run whenever a Flask app is created (per-thread/per-process)
|
||||
@classmethod
|
||||
def run_webhost_app_setup(cls, app: "Flask"):
|
||||
pass
|
||||
|
||||
class World(metaclass=AutoWorldRegister):
|
||||
"""A World object encompasses a game's Items, Locations, Rules and additional data or functionality required.
|
||||
@@ -203,6 +244,14 @@ class World(metaclass=AutoWorldRegister):
|
||||
location_names: ClassVar[Set[str]]
|
||||
"""set of all potential location names"""
|
||||
|
||||
random: random.Random
|
||||
"""This world's random object. Should be used for any randomization needed in world for this player slot."""
|
||||
|
||||
settings_key: ClassVar[str]
|
||||
"""name of the section in host.yaml for world-specific settings, will default to {folder}_options"""
|
||||
settings: ClassVar[Optional["Group"]]
|
||||
"""loaded settings from host.yaml"""
|
||||
|
||||
zip_path: ClassVar[Optional[pathlib.Path]] = None
|
||||
"""If loaded from a .apworld, this is the Path to it."""
|
||||
__file__: ClassVar[str]
|
||||
@@ -212,6 +261,11 @@ class World(metaclass=AutoWorldRegister):
|
||||
self.multiworld = multiworld
|
||||
self.player = player
|
||||
|
||||
def __getattr__(self, item: str) -> Any:
|
||||
if item == "settings":
|
||||
return self.__class__.settings
|
||||
raise AttributeError
|
||||
|
||||
# overridable methods that get called by Main.py, sorted by execution order
|
||||
# can also be implemented as a classmethod and called "stage_<original_name>",
|
||||
# in that case the MultiWorld object is passed as an argument and it gets called once for the entire multiworld.
|
||||
@@ -269,8 +323,8 @@ class World(metaclass=AutoWorldRegister):
|
||||
This happens before progression balancing, so the items may not be in their final locations yet."""
|
||||
|
||||
def generate_output(self, output_directory: str) -> None:
|
||||
"""This method gets called from a threadpool, do not use world.random here.
|
||||
If you need any last-second randomization, use MultiWorld.per_slot_randoms[slot] instead."""
|
||||
"""This method gets called from a threadpool, do not use multiworld.random here.
|
||||
If you need any last-second randomization, use self.random instead."""
|
||||
pass
|
||||
|
||||
def fill_slot_data(self) -> Dict[str, Any]: # json of WebHostLib.models.Slot
|
||||
@@ -372,7 +426,6 @@ class World(metaclass=AutoWorldRegister):
|
||||
res["checksum"] = data_package_checksum(res)
|
||||
return res
|
||||
|
||||
|
||||
# any methods attached to this can be used as part of CollectionState,
|
||||
# please use a prefix as all of them get clobbered together
|
||||
class LogicMixin(metaclass=AutoLogicRegister):
|
||||
|
||||
@@ -3,6 +3,8 @@ import copy
|
||||
import itertools
|
||||
import math
|
||||
import os
|
||||
import settings
|
||||
import typing
|
||||
from enum import IntFlag
|
||||
from typing import Any, ClassVar, Dict, List, Optional, Set, Tuple
|
||||
|
||||
@@ -31,6 +33,42 @@ from worlds.LauncherComponents import Component, components, SuffixIdentifier
|
||||
components.append(Component('Adventure Client', 'AdventureClient', file_identifier=SuffixIdentifier('.apadvn')))
|
||||
|
||||
|
||||
class AdventureSettings(settings.Group):
|
||||
class RomFile(settings.UserFilePath):
|
||||
"""
|
||||
File name of the standard NTSC Adventure rom.
|
||||
The licensed "The 80 Classic Games" CD-ROM contains this.
|
||||
It may also have a .a26 extension
|
||||
"""
|
||||
copy_to = "ADVNTURE.BIN"
|
||||
description = "Adventure ROM File"
|
||||
md5s = [AdventureDeltaPatch.hash]
|
||||
|
||||
class RomStart(str):
|
||||
"""
|
||||
Set this to false to never autostart a rom (such as after patching)
|
||||
True for operating system default program for '.a26'
|
||||
Alternatively, a path to a program to open the .a26 file with (generally EmuHawk for multiworld)
|
||||
"""
|
||||
|
||||
class RomArgs(str):
|
||||
"""
|
||||
Optional, additional args passed into rom_start before the .bin file
|
||||
For example, this can be used to autoload the connector script in BizHawk
|
||||
(see BizHawk --lua= option)
|
||||
Windows example:
|
||||
rom_args: "--lua=C:/ProgramData/Archipelago/data/lua/connector_adventure.lua"
|
||||
"""
|
||||
|
||||
class DisplayMsgs(settings.Bool):
|
||||
"""Set this to true to display item received messages in EmuHawk"""
|
||||
|
||||
rom_file: RomFile = RomFile(RomFile.copy_to)
|
||||
rom_start: typing.Union[RomStart, bool] = True
|
||||
rom_args: Optional[RomArgs] = " "
|
||||
display_msgs: typing.Union[DisplayMsgs, bool] = True
|
||||
|
||||
|
||||
class AdventureWeb(WebWorld):
|
||||
theme = "dirt"
|
||||
|
||||
@@ -53,7 +91,6 @@ class AdventureWeb(WebWorld):
|
||||
)
|
||||
|
||||
tutorials = [setup, setup_fr]
|
||||
|
||||
|
||||
|
||||
def get_item_position_data_start(table_index: int):
|
||||
@@ -73,6 +110,7 @@ class AdventureWorld(World):
|
||||
web: ClassVar[WebWorld] = AdventureWeb()
|
||||
|
||||
option_definitions: ClassVar[Dict[str, AssembleOptions]] = adventure_option_definitions
|
||||
settings: ClassVar[AdventureSettings]
|
||||
item_name_to_id: ClassVar[Dict[str, int]] = {name: data.id for name, data in item_table.items()}
|
||||
location_name_to_id: ClassVar[Dict[str, int]] = {name: data.location_id for name, data in location_table.items()}
|
||||
data_version: ClassVar[int] = 1
|
||||
|
||||
@@ -581,31 +581,25 @@ class ALTTPSNIClient(SNIClient):
|
||||
def get_alttp_settings(romfile: str):
|
||||
import LttPAdjuster
|
||||
|
||||
last_settings = Utils.get_adjuster_settings(GAME_ALTTP)
|
||||
base_settings = LttPAdjuster.get_argparser().parse_known_args(args=[])[0]
|
||||
allow_list = {"music", "menuspeed", "heartbeep", "heartcolor", "ow_palettes", "quickswap",
|
||||
"uw_palettes", "sprite", "sword_palettes", "shield_palettes", "hud_palettes",
|
||||
"reduceflashing", "deathlink", "allowcollect", "oof"}
|
||||
|
||||
for option_name in allow_list:
|
||||
# set new defaults since last_settings were created
|
||||
if not hasattr(last_settings, option_name):
|
||||
setattr(last_settings, option_name, getattr(base_settings, option_name))
|
||||
|
||||
adjustedromfile = ''
|
||||
if last_settings:
|
||||
if vars(Utils.get_adjuster_settings_no_defaults(GAME_ALTTP)):
|
||||
last_settings = Utils.get_adjuster_settings(GAME_ALTTP)
|
||||
|
||||
allow_list = {"music", "menuspeed", "heartbeep", "heartcolor", "ow_palettes", "quickswap",
|
||||
"uw_palettes", "sprite", "sword_palettes", "shield_palettes", "hud_palettes",
|
||||
"reduceflashing", "deathlink", "allowcollect", "oof"}
|
||||
choice = 'no'
|
||||
if not hasattr(last_settings, 'auto_apply') or 'ask' in last_settings.auto_apply:
|
||||
if 'ask' in last_settings.auto_apply:
|
||||
printed_options = {name: value for name, value in vars(last_settings).items() if name in allow_list}
|
||||
if hasattr(last_settings, "sprite_pool"):
|
||||
sprite_pool = {}
|
||||
for sprite in last_settings.sprite_pool:
|
||||
if sprite in sprite_pool:
|
||||
sprite_pool[sprite] += 1
|
||||
else:
|
||||
sprite_pool[sprite] = 1
|
||||
if sprite_pool:
|
||||
printed_options["sprite_pool"] = sprite_pool
|
||||
|
||||
sprite_pool = {}
|
||||
for sprite in last_settings.sprite_pool:
|
||||
if sprite in sprite_pool:
|
||||
sprite_pool[sprite] += 1
|
||||
else:
|
||||
sprite_pool[sprite] = 1
|
||||
if sprite_pool:
|
||||
printed_options["sprite_pool"] = sprite_pool
|
||||
import pprint
|
||||
|
||||
from CommonClient import gui_enabled
|
||||
@@ -685,17 +679,17 @@ def get_alttp_settings(romfile: str):
|
||||
choice = 'yes'
|
||||
|
||||
if 'yes' in choice:
|
||||
import LttPAdjuster
|
||||
from worlds.alttp.Rom import get_base_rom_path
|
||||
last_settings.rom = romfile
|
||||
last_settings.baserom = get_base_rom_path()
|
||||
last_settings.world = None
|
||||
|
||||
if hasattr(last_settings, "sprite_pool"):
|
||||
if last_settings.sprite_pool:
|
||||
from LttPAdjuster import AdjusterWorld
|
||||
last_settings.world = AdjusterWorld(getattr(last_settings, "sprite_pool"))
|
||||
|
||||
adjusted = True
|
||||
import LttPAdjuster
|
||||
_, adjustedromfile = LttPAdjuster.adjust(last_settings)
|
||||
|
||||
if hasattr(last_settings, "world"):
|
||||
|
||||
@@ -130,7 +130,7 @@ difficulties = {
|
||||
progressiveshield=['Progressive Shield'] * 3,
|
||||
basicshield=['Blue Shield', 'Red Shield', 'Red Shield'],
|
||||
progressivearmor=['Progressive Mail'] * 2,
|
||||
basicarmor=['Blue Mail', 'Blue Mail'] * 2,
|
||||
basicarmor=['Blue Mail'] * 2,
|
||||
swordless=['Rupees (20)'] * 4,
|
||||
progressivemagic=['Magic Upgrade (1/2)', 'Rupees (300)'],
|
||||
basicmagic=['Magic Upgrade (1/2)', 'Rupees (300)'],
|
||||
|
||||
@@ -7,21 +7,19 @@ LTTPJPN10HASH: str = "03a63945398191337e896e5771f77173"
|
||||
RANDOMIZERBASEHASH: str = "9952c2a3ec1b421e408df0d20c8f0c7f"
|
||||
ROM_PLAYER_LIMIT: int = 255
|
||||
|
||||
import io
|
||||
import json
|
||||
import hashlib
|
||||
import logging
|
||||
import os
|
||||
import random
|
||||
import struct
|
||||
import subprocess
|
||||
import threading
|
||||
import concurrent.futures
|
||||
import bsdiff4
|
||||
from typing import Optional, List
|
||||
from typing import List
|
||||
|
||||
from BaseClasses import CollectionState, Region, Location, MultiWorld
|
||||
from Utils import local_path, user_path, int16_as_bytes, int32_as_bytes, snes_to_pc, is_frozen, parse_yaml, read_snes_rom
|
||||
from Utils import local_path, user_path, int16_as_bytes, int32_as_bytes, snes_to_pc, is_frozen, read_snes_rom
|
||||
|
||||
from .Shops import ShopType, ShopPriceType
|
||||
from .Dungeons import dungeon_music_addresses
|
||||
@@ -37,6 +35,7 @@ from .Text import KingsReturn_texts, Sanctuary_texts, Kakariko_texts, Blacksmith
|
||||
from .Items import ItemFactory, item_table, item_name_groups, progression_items
|
||||
from .EntranceShuffle import door_addresses
|
||||
from .Options import smallkey_shuffle
|
||||
from .Sprites import apply_random_sprite_on_event
|
||||
|
||||
try:
|
||||
from maseya import z3pr
|
||||
@@ -212,73 +211,6 @@ def check_enemizer(enemizercli):
|
||||
check_enemizer.done = True
|
||||
|
||||
|
||||
def apply_random_sprite_on_event(rom: LocalRom, sprite, local_random, allow_random_on_event, sprite_pool):
|
||||
userandomsprites = False
|
||||
if sprite and not isinstance(sprite, Sprite):
|
||||
sprite = sprite.lower()
|
||||
userandomsprites = sprite.startswith('randomon')
|
||||
|
||||
racerom = rom.read_byte(0x180213)
|
||||
if allow_random_on_event or not racerom:
|
||||
# Changes to this byte for race rom seeds are only permitted on initial rolling of the seed.
|
||||
# However, if the seed is not a racerom seed, then it is always allowed.
|
||||
rom.write_byte(0x186381, 0x00 if userandomsprites else 0x01)
|
||||
|
||||
onevent = 0
|
||||
if sprite == 'randomonall':
|
||||
onevent = 0xFFFF # Support all current and future events that can cause random sprite changes.
|
||||
elif sprite == 'randomonnone':
|
||||
# Allows for opting into random on events on race rom seeds, without actually enabling any of the events initially.
|
||||
onevent = 0x0000
|
||||
elif sprite == 'randomonrandom':
|
||||
# Allows random to take the wheel on which events apply. (at least one event will be applied.)
|
||||
onevent = local_random.randint(0x0001, 0x003F)
|
||||
elif userandomsprites:
|
||||
onevent = 0x01 if 'hit' in sprite else 0x00
|
||||
onevent += 0x02 if 'enter' in sprite else 0x00
|
||||
onevent += 0x04 if 'exit' in sprite else 0x00
|
||||
onevent += 0x08 if 'slash' in sprite else 0x00
|
||||
onevent += 0x10 if 'item' in sprite else 0x00
|
||||
onevent += 0x20 if 'bonk' in sprite else 0x00
|
||||
|
||||
rom.write_int16(0x18637F, onevent)
|
||||
|
||||
sprite = Sprite(sprite) if os.path.isfile(sprite) else Sprite.get_sprite_from_name(sprite, local_random)
|
||||
|
||||
# write link sprite if required
|
||||
if sprite:
|
||||
sprites = list()
|
||||
sprite.write_to_rom(rom)
|
||||
|
||||
_populate_sprite_table()
|
||||
if userandomsprites:
|
||||
if sprite_pool:
|
||||
if isinstance(sprite_pool, str):
|
||||
sprite_pool = sprite_pool.split(':')
|
||||
for spritename in sprite_pool:
|
||||
sprite = Sprite(spritename) if os.path.isfile(spritename) else Sprite.get_sprite_from_name(
|
||||
spritename, local_random)
|
||||
if sprite:
|
||||
sprites.append(sprite)
|
||||
else:
|
||||
logging.info(f"Sprite {spritename} was not found.")
|
||||
else:
|
||||
sprites = list(set(_sprite_table.values())) # convert to list and remove dupes
|
||||
else:
|
||||
sprites.append(sprite)
|
||||
if sprites:
|
||||
while len(sprites) < 32:
|
||||
sprites.extend(sprites)
|
||||
local_random.shuffle(sprites)
|
||||
|
||||
for i, sprite in enumerate(sprites[:32]):
|
||||
if not i and not userandomsprites:
|
||||
continue
|
||||
rom.write_bytes(0x300000 + (i * 0x8000), sprite.sprite)
|
||||
rom.write_bytes(0x307000 + (i * 0x8000), sprite.palette)
|
||||
rom.write_bytes(0x307078 + (i * 0x8000), sprite.glove_palette)
|
||||
|
||||
|
||||
def patch_enemizer(world, rom: LocalRom, enemizercli, output_directory):
|
||||
player = world.player
|
||||
multiworld = world.multiworld
|
||||
@@ -487,271 +419,6 @@ class TileSet:
|
||||
return localrandom.choice(tile_sets)
|
||||
|
||||
|
||||
sprite_list_lock = threading.Lock()
|
||||
_sprite_table = {}
|
||||
|
||||
|
||||
def _populate_sprite_table():
|
||||
with sprite_list_lock:
|
||||
if not _sprite_table:
|
||||
def load_sprite_from_file(file):
|
||||
sprite = Sprite(file)
|
||||
if sprite.valid:
|
||||
_sprite_table[sprite.name.lower()] = sprite
|
||||
_sprite_table[os.path.basename(file).split(".")[0].lower()] = sprite # alias for filename base
|
||||
else:
|
||||
logging.debug(f"Spritefile {file} could not be loaded as a valid sprite.")
|
||||
|
||||
with concurrent.futures.ThreadPoolExecutor() as pool:
|
||||
for dir in [user_path('data', 'sprites', 'alttpr'), user_path('data', 'sprites', 'custom')]:
|
||||
for file in os.listdir(dir):
|
||||
pool.submit(load_sprite_from_file, os.path.join(dir, file))
|
||||
|
||||
|
||||
class Sprite():
|
||||
sprite_size = 28672
|
||||
palette_size = 120
|
||||
glove_size = 4
|
||||
author_name: Optional[str] = None
|
||||
base_data: bytes
|
||||
|
||||
def __init__(self, filename):
|
||||
if not hasattr(Sprite, "base_data"):
|
||||
self.get_vanilla_sprite_data()
|
||||
with open(filename, 'rb') as file:
|
||||
filedata = file.read()
|
||||
self.name = os.path.basename(filename)
|
||||
self.valid = True
|
||||
if filename.endswith(".apsprite"):
|
||||
self.from_ap_sprite(filedata)
|
||||
elif len(filedata) == 0x7000:
|
||||
# sprite file with graphics and without palette data
|
||||
self.sprite = filedata[:0x7000]
|
||||
elif len(filedata) == 0x7078:
|
||||
# sprite file with graphics and palette data
|
||||
self.sprite = filedata[:0x7000]
|
||||
self.palette = filedata[0x7000:]
|
||||
self.glove_palette = filedata[0x7036:0x7038] + filedata[0x7054:0x7056]
|
||||
elif len(filedata) == 0x707C:
|
||||
# sprite file with graphics and palette data including gloves
|
||||
self.sprite = filedata[:0x7000]
|
||||
self.palette = filedata[0x7000:0x7078]
|
||||
self.glove_palette = filedata[0x7078:]
|
||||
elif len(filedata) in [0x100000, 0x200000, 0x400000]:
|
||||
# full rom with patched sprite, extract it
|
||||
self.sprite = filedata[0x80000:0x87000]
|
||||
self.palette = filedata[0xDD308:0xDD380]
|
||||
self.glove_palette = filedata[0xDEDF5:0xDEDF9]
|
||||
elif filedata.startswith(b'ZSPR'):
|
||||
self.from_zspr(filedata, filename)
|
||||
else:
|
||||
self.valid = False
|
||||
|
||||
def get_vanilla_sprite_data(self):
|
||||
file_name = get_base_rom_path()
|
||||
base_rom_bytes = bytes(read_snes_rom(open(file_name, "rb")))
|
||||
Sprite.sprite = base_rom_bytes[0x80000:0x87000]
|
||||
Sprite.palette = base_rom_bytes[0xDD308:0xDD380]
|
||||
Sprite.glove_palette = base_rom_bytes[0xDEDF5:0xDEDF9]
|
||||
Sprite.base_data = Sprite.sprite + Sprite.palette + Sprite.glove_palette
|
||||
|
||||
def from_ap_sprite(self, filedata):
|
||||
# noinspection PyBroadException
|
||||
try:
|
||||
obj = parse_yaml(filedata.decode("utf-8-sig"))
|
||||
if obj["min_format_version"] > 1:
|
||||
raise Exception("Sprite file requires an updated reader.")
|
||||
self.author_name = obj["author"]
|
||||
self.name = obj["name"]
|
||||
if obj["data"]: # skip patching for vanilla content
|
||||
data = bsdiff4.patch(Sprite.base_data, obj["data"])
|
||||
self.sprite = data[:self.sprite_size]
|
||||
self.palette = data[self.sprite_size:self.palette_size]
|
||||
self.glove_palette = data[self.sprite_size + self.palette_size:]
|
||||
except Exception:
|
||||
logger = logging.getLogger("apsprite")
|
||||
logger.exception("Error parsing apsprite file")
|
||||
self.valid = False
|
||||
|
||||
@property
|
||||
def author_game_display(self) -> str:
|
||||
name = getattr(self, "_author_game_display", "")
|
||||
if not name:
|
||||
name = self.author_name
|
||||
|
||||
# At this point, may need some filtering to displayable characters
|
||||
return name
|
||||
|
||||
def to_ap_sprite(self, path):
|
||||
import yaml
|
||||
payload = {"format_version": 1,
|
||||
"min_format_version": 1,
|
||||
"sprite_version": 1,
|
||||
"name": self.name,
|
||||
"author": self.author_name,
|
||||
"game": "A Link to the Past",
|
||||
"data": self.get_delta()}
|
||||
with open(path, "w") as f:
|
||||
f.write(yaml.safe_dump(payload))
|
||||
|
||||
def get_delta(self):
|
||||
modified_data = self.sprite + self.palette + self.glove_palette
|
||||
return bsdiff4.diff(Sprite.base_data, modified_data)
|
||||
|
||||
def from_zspr(self, filedata, filename):
|
||||
result = self.parse_zspr(filedata, 1)
|
||||
if result is None:
|
||||
self.valid = False
|
||||
return
|
||||
(sprite, palette, self.name, self.author_name, self._author_game_display) = result
|
||||
if self.name == "":
|
||||
self.name = os.path.split(filename)[1].split(".")[0]
|
||||
|
||||
if len(sprite) != 0x7000:
|
||||
self.valid = False
|
||||
return
|
||||
self.sprite = sprite
|
||||
if len(palette) == 0:
|
||||
pass
|
||||
elif len(palette) == 0x78:
|
||||
self.palette = palette
|
||||
elif len(palette) == 0x7C:
|
||||
self.palette = palette[:0x78]
|
||||
self.glove_palette = palette[0x78:]
|
||||
else:
|
||||
self.valid = False
|
||||
|
||||
@staticmethod
|
||||
def get_sprite_from_name(name: str, local_random=random) -> Optional[Sprite]:
|
||||
_populate_sprite_table()
|
||||
name = name.lower()
|
||||
if name.startswith('random'):
|
||||
sprites = list(set(_sprite_table.values()))
|
||||
sprites.sort(key=lambda x: x.name)
|
||||
return local_random.choice(sprites)
|
||||
return _sprite_table.get(name, None)
|
||||
|
||||
@staticmethod
|
||||
def default_link_sprite():
|
||||
return Sprite(local_path('data', 'default.apsprite'))
|
||||
|
||||
def decode8(self, pos):
|
||||
arr = [[0 for _ in range(8)] for _ in range(8)]
|
||||
for y in range(8):
|
||||
for x in range(8):
|
||||
position = 1 << (7 - x)
|
||||
val = 0
|
||||
if self.sprite[pos + 2 * y] & position:
|
||||
val += 1
|
||||
if self.sprite[pos + 2 * y + 1] & position:
|
||||
val += 2
|
||||
if self.sprite[pos + 2 * y + 16] & position:
|
||||
val += 4
|
||||
if self.sprite[pos + 2 * y + 17] & position:
|
||||
val += 8
|
||||
arr[y][x] = val
|
||||
return arr
|
||||
|
||||
def decode16(self, pos):
|
||||
arr = [[0 for _ in range(16)] for _ in range(16)]
|
||||
top_left = self.decode8(pos)
|
||||
top_right = self.decode8(pos + 0x20)
|
||||
bottom_left = self.decode8(pos + 0x200)
|
||||
bottom_right = self.decode8(pos + 0x220)
|
||||
for x in range(8):
|
||||
for y in range(8):
|
||||
arr[y][x] = top_left[y][x]
|
||||
arr[y][x + 8] = top_right[y][x]
|
||||
arr[y + 8][x] = bottom_left[y][x]
|
||||
arr[y + 8][x + 8] = bottom_right[y][x]
|
||||
return arr
|
||||
|
||||
@staticmethod
|
||||
def parse_zspr(filedata, expected_kind):
|
||||
logger = logging.getLogger("ZSPR")
|
||||
headerstr = "<4xBHHIHIHH6x"
|
||||
headersize = struct.calcsize(headerstr)
|
||||
if len(filedata) < headersize:
|
||||
return None
|
||||
version, csum, icsum, sprite_offset, sprite_size, palette_offset, palette_size, kind = struct.unpack_from(
|
||||
headerstr, filedata)
|
||||
if version not in [1]:
|
||||
logger.error("Error parsing ZSPR file: Version %g not supported", version)
|
||||
return None
|
||||
if kind != expected_kind:
|
||||
return None
|
||||
|
||||
stream = io.BytesIO(filedata)
|
||||
stream.seek(headersize)
|
||||
|
||||
def read_utf16le(stream):
|
||||
"""Decodes a null-terminated UTF-16_LE string of unknown size from a stream"""
|
||||
raw = bytearray()
|
||||
while True:
|
||||
char = stream.read(2)
|
||||
if char in [b"", b"\x00\x00"]:
|
||||
break
|
||||
raw += char
|
||||
return raw.decode("utf-16_le")
|
||||
|
||||
# noinspection PyBroadException
|
||||
try:
|
||||
sprite_name = read_utf16le(stream)
|
||||
author_name = read_utf16le(stream)
|
||||
author_credits_name = stream.read().split(b"\x00", 1)[0].decode()
|
||||
|
||||
# Ignoring the Author Rom name for the time being.
|
||||
|
||||
real_csum = sum(filedata) % 0x10000
|
||||
if real_csum != csum or real_csum ^ 0xFFFF != icsum:
|
||||
logger.warning("ZSPR file has incorrect checksum. It may be corrupted.")
|
||||
|
||||
sprite = filedata[sprite_offset:sprite_offset + sprite_size]
|
||||
palette = filedata[palette_offset:palette_offset + palette_size]
|
||||
|
||||
if len(sprite) != sprite_size or len(palette) != palette_size:
|
||||
logger.error("Error parsing ZSPR file: Unexpected end of file")
|
||||
return None
|
||||
|
||||
return sprite, palette, sprite_name, author_name, author_credits_name
|
||||
|
||||
except Exception:
|
||||
logger.exception("Error parsing ZSPR file")
|
||||
return None
|
||||
|
||||
def decode_palette(self):
|
||||
"""Returns the palettes as an array of arrays of 15 colors"""
|
||||
|
||||
def array_chunk(arr, size):
|
||||
return list(zip(*[iter(arr)] * size))
|
||||
|
||||
def make_int16(pair):
|
||||
return pair[1] << 8 | pair[0]
|
||||
|
||||
def expand_color(i):
|
||||
return (i & 0x1F) * 8, (i >> 5 & 0x1F) * 8, (i >> 10 & 0x1F) * 8
|
||||
|
||||
# turn palette data into a list of RGB tuples with 8 bit values
|
||||
palette_as_colors = [expand_color(make_int16(chnk)) for chnk in array_chunk(self.palette, 2)]
|
||||
|
||||
# split into palettes of 15 colors
|
||||
return array_chunk(palette_as_colors, 15)
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self.name)
|
||||
|
||||
def write_to_rom(self, rom: LocalRom):
|
||||
if not self.valid:
|
||||
logging.warning("Tried writing invalid sprite to rom, skipping.")
|
||||
return
|
||||
rom.write_bytes(0x80000, self.sprite)
|
||||
rom.write_bytes(0xDD308, self.palette)
|
||||
rom.write_bytes(0xDEDF5, self.glove_palette)
|
||||
rom.write_bytes(0x300000, self.sprite)
|
||||
rom.write_bytes(0x307000, self.palette)
|
||||
rom.write_bytes(0x307078, self.glove_palette)
|
||||
|
||||
|
||||
bonk_addresses = [0x4CF6C, 0x4CFBA, 0x4CFE0, 0x4CFFB, 0x4D018, 0x4D01B, 0x4D028, 0x4D03C, 0x4D059, 0x4D07A,
|
||||
0x4D09E, 0x4D0A8, 0x4D0AB, 0x4D0AE, 0x4D0BE, 0x4D0DD,
|
||||
0x4D16A, 0x4D1E5, 0x4D1EE, 0x4D20B, 0x4CBBF, 0x4CBBF, 0x4CC17, 0x4CC1A, 0x4CC4A, 0x4CC4D,
|
||||
|
||||
@@ -172,6 +172,7 @@ def FillDisabledShopSlots(world):
|
||||
shop: Shop = location.parent_region.shop
|
||||
location.item = ItemFactory(shop.inventory[location.shop_slot]['item'], location.player)
|
||||
location.item_rule = lambda item: item.name == location.item.name and item.player == location.player
|
||||
location.locked = True
|
||||
|
||||
|
||||
def ShopSlotFill(multiworld):
|
||||
@@ -278,6 +279,8 @@ def ShopSlotFill(multiworld):
|
||||
if 'P' in multiworld.shop_shuffle[location.player]:
|
||||
price_to_funny_price(multiworld, shop.inventory[location.shop_slot], location.player)
|
||||
|
||||
FillDisabledShopSlots(multiworld)
|
||||
|
||||
|
||||
def create_shops(world, player: int):
|
||||
option = world.shop_shuffle[player]
|
||||
|
||||
393
worlds/alttp/Sprites.py
Normal file
393
worlds/alttp/Sprites.py
Normal file
@@ -0,0 +1,393 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import concurrent.futures
|
||||
import io
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import random
|
||||
import struct
|
||||
import threading
|
||||
from typing import Optional, TYPE_CHECKING
|
||||
|
||||
import bsdiff4
|
||||
|
||||
from Utils import user_path, read_snes_rom, parse_yaml, local_path
|
||||
if TYPE_CHECKING:
|
||||
from .Rom import LocalRom
|
||||
|
||||
sprite_list_lock = threading.Lock()
|
||||
_sprite_table = {}
|
||||
|
||||
|
||||
def _populate_sprite_table():
|
||||
with sprite_list_lock:
|
||||
if not _sprite_table:
|
||||
def load_sprite_from_file(file):
|
||||
sprite = Sprite(file)
|
||||
if sprite.valid:
|
||||
_sprite_table[sprite.name.lower()] = sprite
|
||||
_sprite_table[os.path.basename(file).split(".")[0].lower()] = sprite # alias for filename base
|
||||
else:
|
||||
logging.debug(f"Spritefile {file} could not be loaded as a valid sprite.")
|
||||
|
||||
with concurrent.futures.ThreadPoolExecutor() as pool:
|
||||
for dir in [user_path('data', 'sprites', 'alttpr'), user_path('data', 'sprites', 'custom')]:
|
||||
for file in os.listdir(dir):
|
||||
pool.submit(load_sprite_from_file, os.path.join(dir, file))
|
||||
|
||||
|
||||
class Sprite():
|
||||
sprite_size = 28672
|
||||
palette_size = 120
|
||||
glove_size = 4
|
||||
author_name: Optional[str] = None
|
||||
base_data: bytes
|
||||
|
||||
def __init__(self, filename):
|
||||
if not hasattr(Sprite, "base_data"):
|
||||
self.get_vanilla_sprite_data()
|
||||
with open(filename, 'rb') as file:
|
||||
filedata = file.read()
|
||||
self.name = os.path.basename(filename)
|
||||
self.valid = True
|
||||
if filename.endswith(".apsprite"):
|
||||
self.from_ap_sprite(filedata)
|
||||
elif len(filedata) == 0x7000:
|
||||
# sprite file with graphics and without palette data
|
||||
self.sprite = filedata[:0x7000]
|
||||
elif len(filedata) == 0x7078:
|
||||
# sprite file with graphics and palette data
|
||||
self.sprite = filedata[:0x7000]
|
||||
self.palette = filedata[0x7000:]
|
||||
self.glove_palette = filedata[0x7036:0x7038] + filedata[0x7054:0x7056]
|
||||
elif len(filedata) == 0x707C:
|
||||
# sprite file with graphics and palette data including gloves
|
||||
self.sprite = filedata[:0x7000]
|
||||
self.palette = filedata[0x7000:0x7078]
|
||||
self.glove_palette = filedata[0x7078:]
|
||||
elif len(filedata) in [0x100000, 0x200000, 0x400000]:
|
||||
# full rom with patched sprite, extract it
|
||||
self.sprite = filedata[0x80000:0x87000]
|
||||
self.palette = filedata[0xDD308:0xDD380]
|
||||
self.glove_palette = filedata[0xDEDF5:0xDEDF9]
|
||||
elif filedata.startswith(b'ZSPR'):
|
||||
self.from_zspr(filedata, filename)
|
||||
else:
|
||||
self.valid = False
|
||||
|
||||
def get_vanilla_sprite_data(self):
|
||||
from .Rom import get_base_rom_path
|
||||
file_name = get_base_rom_path()
|
||||
base_rom_bytes = bytes(read_snes_rom(open(file_name, "rb")))
|
||||
Sprite.sprite = base_rom_bytes[0x80000:0x87000]
|
||||
Sprite.palette = base_rom_bytes[0xDD308:0xDD380]
|
||||
Sprite.glove_palette = base_rom_bytes[0xDEDF5:0xDEDF9]
|
||||
Sprite.base_data = Sprite.sprite + Sprite.palette + Sprite.glove_palette
|
||||
|
||||
def from_ap_sprite(self, filedata):
|
||||
# noinspection PyBroadException
|
||||
try:
|
||||
obj = parse_yaml(filedata.decode("utf-8-sig"))
|
||||
if obj["min_format_version"] > 1:
|
||||
raise Exception("Sprite file requires an updated reader.")
|
||||
self.author_name = obj["author"]
|
||||
self.name = obj["name"]
|
||||
if obj["data"]: # skip patching for vanilla content
|
||||
data = bsdiff4.patch(Sprite.base_data, obj["data"])
|
||||
self.sprite = data[:self.sprite_size]
|
||||
self.palette = data[self.sprite_size:self.palette_size]
|
||||
self.glove_palette = data[self.sprite_size + self.palette_size:]
|
||||
except Exception:
|
||||
logger = logging.getLogger("apsprite")
|
||||
logger.exception("Error parsing apsprite file")
|
||||
self.valid = False
|
||||
|
||||
@property
|
||||
def author_game_display(self) -> str:
|
||||
name = getattr(self, "_author_game_display", "")
|
||||
if not name:
|
||||
name = self.author_name
|
||||
|
||||
# At this point, may need some filtering to displayable characters
|
||||
return name
|
||||
|
||||
def to_ap_sprite(self, path):
|
||||
import yaml
|
||||
payload = {"format_version": 1,
|
||||
"min_format_version": 1,
|
||||
"sprite_version": 1,
|
||||
"name": self.name,
|
||||
"author": self.author_name,
|
||||
"game": "A Link to the Past",
|
||||
"data": self.get_delta()}
|
||||
with open(path, "w") as f:
|
||||
f.write(yaml.safe_dump(payload))
|
||||
|
||||
def get_delta(self):
|
||||
modified_data = self.sprite + self.palette + self.glove_palette
|
||||
return bsdiff4.diff(Sprite.base_data, modified_data)
|
||||
|
||||
def from_zspr(self, filedata, filename):
|
||||
result = self.parse_zspr(filedata, 1)
|
||||
if result is None:
|
||||
self.valid = False
|
||||
return
|
||||
(sprite, palette, self.name, self.author_name, self._author_game_display) = result
|
||||
if self.name == "":
|
||||
self.name = os.path.split(filename)[1].split(".")[0]
|
||||
|
||||
if len(sprite) != 0x7000:
|
||||
self.valid = False
|
||||
return
|
||||
self.sprite = sprite
|
||||
if len(palette) == 0:
|
||||
pass
|
||||
elif len(palette) == 0x78:
|
||||
self.palette = palette
|
||||
elif len(palette) == 0x7C:
|
||||
self.palette = palette[:0x78]
|
||||
self.glove_palette = palette[0x78:]
|
||||
else:
|
||||
self.valid = False
|
||||
|
||||
@staticmethod
|
||||
def get_sprite_from_name(name: str, local_random=random) -> Optional[Sprite]:
|
||||
_populate_sprite_table()
|
||||
name = name.lower()
|
||||
if name.startswith('random'):
|
||||
sprites = list(set(_sprite_table.values()))
|
||||
sprites.sort(key=lambda x: x.name)
|
||||
return local_random.choice(sprites)
|
||||
return _sprite_table.get(name, None)
|
||||
|
||||
@staticmethod
|
||||
def default_link_sprite():
|
||||
return Sprite(local_path('data', 'default.apsprite'))
|
||||
|
||||
def decode8(self, pos):
|
||||
arr = [[0 for _ in range(8)] for _ in range(8)]
|
||||
for y in range(8):
|
||||
for x in range(8):
|
||||
position = 1 << (7 - x)
|
||||
val = 0
|
||||
if self.sprite[pos + 2 * y] & position:
|
||||
val += 1
|
||||
if self.sprite[pos + 2 * y + 1] & position:
|
||||
val += 2
|
||||
if self.sprite[pos + 2 * y + 16] & position:
|
||||
val += 4
|
||||
if self.sprite[pos + 2 * y + 17] & position:
|
||||
val += 8
|
||||
arr[y][x] = val
|
||||
return arr
|
||||
|
||||
def decode16(self, pos):
|
||||
arr = [[0 for _ in range(16)] for _ in range(16)]
|
||||
top_left = self.decode8(pos)
|
||||
top_right = self.decode8(pos + 0x20)
|
||||
bottom_left = self.decode8(pos + 0x200)
|
||||
bottom_right = self.decode8(pos + 0x220)
|
||||
for x in range(8):
|
||||
for y in range(8):
|
||||
arr[y][x] = top_left[y][x]
|
||||
arr[y][x + 8] = top_right[y][x]
|
||||
arr[y + 8][x] = bottom_left[y][x]
|
||||
arr[y + 8][x + 8] = bottom_right[y][x]
|
||||
return arr
|
||||
|
||||
@staticmethod
|
||||
def parse_zspr(filedata, expected_kind):
|
||||
logger = logging.getLogger("ZSPR")
|
||||
headerstr = "<4xBHHIHIHH6x"
|
||||
headersize = struct.calcsize(headerstr)
|
||||
if len(filedata) < headersize:
|
||||
return None
|
||||
version, csum, icsum, sprite_offset, sprite_size, palette_offset, palette_size, kind = struct.unpack_from(
|
||||
headerstr, filedata)
|
||||
if version not in [1]:
|
||||
logger.error("Error parsing ZSPR file: Version %g not supported", version)
|
||||
return None
|
||||
if kind != expected_kind:
|
||||
return None
|
||||
|
||||
stream = io.BytesIO(filedata)
|
||||
stream.seek(headersize)
|
||||
|
||||
def read_utf16le(stream):
|
||||
"""Decodes a null-terminated UTF-16_LE string of unknown size from a stream"""
|
||||
raw = bytearray()
|
||||
while True:
|
||||
char = stream.read(2)
|
||||
if char in [b"", b"\x00\x00"]:
|
||||
break
|
||||
raw += char
|
||||
return raw.decode("utf-16_le")
|
||||
|
||||
# noinspection PyBroadException
|
||||
try:
|
||||
sprite_name = read_utf16le(stream)
|
||||
author_name = read_utf16le(stream)
|
||||
author_credits_name = stream.read().split(b"\x00", 1)[0].decode()
|
||||
|
||||
# Ignoring the Author Rom name for the time being.
|
||||
|
||||
real_csum = sum(filedata) % 0x10000
|
||||
if real_csum != csum or real_csum ^ 0xFFFF != icsum:
|
||||
logger.warning("ZSPR file has incorrect checksum. It may be corrupted.")
|
||||
|
||||
sprite = filedata[sprite_offset:sprite_offset + sprite_size]
|
||||
palette = filedata[palette_offset:palette_offset + palette_size]
|
||||
|
||||
if len(sprite) != sprite_size or len(palette) != palette_size:
|
||||
logger.error("Error parsing ZSPR file: Unexpected end of file")
|
||||
return None
|
||||
|
||||
return sprite, palette, sprite_name, author_name, author_credits_name
|
||||
|
||||
except Exception:
|
||||
logger.exception("Error parsing ZSPR file")
|
||||
return None
|
||||
|
||||
def decode_palette(self):
|
||||
"""Returns the palettes as an array of arrays of 15 colors"""
|
||||
|
||||
def array_chunk(arr, size):
|
||||
return list(zip(*[iter(arr)] * size))
|
||||
|
||||
def make_int16(pair):
|
||||
return pair[1] << 8 | pair[0]
|
||||
|
||||
def expand_color(i):
|
||||
return (i & 0x1F) * 8, (i >> 5 & 0x1F) * 8, (i >> 10 & 0x1F) * 8
|
||||
|
||||
# turn palette data into a list of RGB tuples with 8 bit values
|
||||
palette_as_colors = [expand_color(make_int16(chnk)) for chnk in array_chunk(self.palette, 2)]
|
||||
|
||||
# split into palettes of 15 colors
|
||||
return array_chunk(palette_as_colors, 15)
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self.name)
|
||||
|
||||
def write_to_rom(self, rom: "LocalRom"):
|
||||
if not self.valid:
|
||||
logging.warning("Tried writing invalid sprite to rom, skipping.")
|
||||
return
|
||||
rom.write_bytes(0x80000, self.sprite)
|
||||
rom.write_bytes(0xDD308, self.palette)
|
||||
rom.write_bytes(0xDEDF5, self.glove_palette)
|
||||
rom.write_bytes(0x300000, self.sprite)
|
||||
rom.write_bytes(0x307000, self.palette)
|
||||
rom.write_bytes(0x307078, self.glove_palette)
|
||||
|
||||
|
||||
def update_sprites():
|
||||
from tkinter import Tk
|
||||
from LttPAdjuster import get_image_for_sprite
|
||||
from LttPAdjuster import BackgroundTaskProgress
|
||||
from LttPAdjuster import BackgroundTaskProgressNullWindow
|
||||
from LttPAdjuster import update_sprites
|
||||
|
||||
# Target directories
|
||||
input_dir = user_path("data", "sprites", "alttpr")
|
||||
output_dir = local_path("WebHostLib", "static", "generated") # TODO: move to user_path
|
||||
|
||||
os.makedirs(os.path.join(output_dir, "sprites"), exist_ok=True)
|
||||
# update sprites through gui.py's functions
|
||||
done = threading.Event()
|
||||
try:
|
||||
top = Tk()
|
||||
except:
|
||||
task = BackgroundTaskProgressNullWindow(update_sprites, lambda successful, resultmessage: done.set())
|
||||
else:
|
||||
top.withdraw()
|
||||
task = BackgroundTaskProgress(top, update_sprites, "Updating Sprites", lambda succesful, resultmessage: done.set())
|
||||
while not done.isSet():
|
||||
task.do_events()
|
||||
|
||||
spriteData = []
|
||||
|
||||
for file in (file for file in os.listdir(input_dir) if not file.startswith(".")):
|
||||
sprite = Sprite(os.path.join(input_dir, file))
|
||||
|
||||
if not sprite.name:
|
||||
print("Warning:", file, "has no name.")
|
||||
sprite.name = file.split(".", 1)[0]
|
||||
if sprite.valid:
|
||||
with open(os.path.join(output_dir, "sprites", f"{os.path.splitext(file)[0]}.gif"), 'wb') as image:
|
||||
image.write(get_image_for_sprite(sprite, True))
|
||||
spriteData.append({"file": file, "author": sprite.author_name, "name": sprite.name})
|
||||
else:
|
||||
print(file, "dropped, as it has no valid sprite data.")
|
||||
spriteData.sort(key=lambda entry: entry["name"])
|
||||
with open(f'{output_dir}/spriteData.json', 'w') as file:
|
||||
json.dump({"sprites": spriteData}, file, indent=1)
|
||||
return spriteData
|
||||
|
||||
|
||||
def apply_random_sprite_on_event(rom: "LocalRom", sprite, local_random, allow_random_on_event, sprite_pool):
|
||||
userandomsprites = False
|
||||
if sprite and not isinstance(sprite, Sprite):
|
||||
sprite = sprite.lower()
|
||||
userandomsprites = sprite.startswith('randomon')
|
||||
|
||||
racerom = rom.read_byte(0x180213)
|
||||
if allow_random_on_event or not racerom:
|
||||
# Changes to this byte for race rom seeds are only permitted on initial rolling of the seed.
|
||||
# However, if the seed is not a racerom seed, then it is always allowed.
|
||||
rom.write_byte(0x186381, 0x00 if userandomsprites else 0x01)
|
||||
|
||||
onevent = 0
|
||||
if sprite == 'randomonall':
|
||||
onevent = 0xFFFF # Support all current and future events that can cause random sprite changes.
|
||||
elif sprite == 'randomonnone':
|
||||
# Allows for opting into random on events on race rom seeds, without actually enabling any of the events initially.
|
||||
onevent = 0x0000
|
||||
elif sprite == 'randomonrandom':
|
||||
# Allows random to take the wheel on which events apply. (at least one event will be applied.)
|
||||
onevent = local_random.randint(0x0001, 0x003F)
|
||||
elif userandomsprites:
|
||||
onevent = 0x01 if 'hit' in sprite else 0x00
|
||||
onevent += 0x02 if 'enter' in sprite else 0x00
|
||||
onevent += 0x04 if 'exit' in sprite else 0x00
|
||||
onevent += 0x08 if 'slash' in sprite else 0x00
|
||||
onevent += 0x10 if 'item' in sprite else 0x00
|
||||
onevent += 0x20 if 'bonk' in sprite else 0x00
|
||||
|
||||
rom.write_int16(0x18637F, onevent)
|
||||
|
||||
sprite = Sprite(sprite) if os.path.isfile(sprite) else Sprite.get_sprite_from_name(sprite, local_random)
|
||||
|
||||
# write link sprite if required
|
||||
if sprite:
|
||||
sprites = list()
|
||||
sprite.write_to_rom(rom)
|
||||
|
||||
_populate_sprite_table()
|
||||
if userandomsprites:
|
||||
if sprite_pool:
|
||||
if isinstance(sprite_pool, str):
|
||||
sprite_pool = sprite_pool.split(':')
|
||||
for spritename in sprite_pool:
|
||||
sprite = Sprite(spritename) if os.path.isfile(spritename) else Sprite.get_sprite_from_name(
|
||||
spritename, local_random)
|
||||
if sprite:
|
||||
sprites.append(sprite)
|
||||
else:
|
||||
logging.info(f"Sprite {spritename} was not found.")
|
||||
else:
|
||||
sprites = list(set(_sprite_table.values())) # convert to list and remove dupes
|
||||
else:
|
||||
sprites.append(sprite)
|
||||
if sprites:
|
||||
while len(sprites) < 32:
|
||||
sprites.extend(sprites)
|
||||
local_random.shuffle(sprites)
|
||||
|
||||
for i, sprite in enumerate(sprites[:32]):
|
||||
if not i and not userandomsprites:
|
||||
continue
|
||||
rom.write_bytes(0x300000 + (i * 0x8000), sprite.sprite)
|
||||
rom.write_bytes(0x307000 + (i * 0x8000), sprite.palette)
|
||||
rom.write_bytes(0x307078 + (i * 0x8000), sprite.glove_palette)
|
||||
@@ -1,6 +1,7 @@
|
||||
import logging
|
||||
import os
|
||||
import random
|
||||
import settings
|
||||
import threading
|
||||
import typing
|
||||
|
||||
@@ -29,6 +30,16 @@ lttp_logger = logging.getLogger("A Link to the Past")
|
||||
extras_list = sum(difficulties['normal'].extras[0:5], [])
|
||||
|
||||
|
||||
class ALTTPSettings(settings.Group):
|
||||
class RomFile(settings.SNESRomPath):
|
||||
"""File name of the v1.0 J rom"""
|
||||
description = "ALTTP v1.0 J ROM File"
|
||||
copy_to = "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc"
|
||||
md5s = [LttPDeltaPatch.hash]
|
||||
|
||||
rom_file: RomFile = RomFile(RomFile.copy_to)
|
||||
|
||||
|
||||
class ALTTPWeb(WebWorld):
|
||||
setup_en = Tutorial(
|
||||
"Multiworld Setup Tutorial",
|
||||
@@ -113,6 +124,14 @@ class ALTTPWeb(WebWorld):
|
||||
|
||||
tutorials = [setup_en, setup_de, setup_es, setup_fr, msu, msu_es, msu_fr, plando, oof_sound]
|
||||
|
||||
@classmethod
|
||||
def run_webhost_setup(cls):
|
||||
rom_file = get_base_rom_path()
|
||||
if os.path.exists(rom_file):
|
||||
from .Sprites import update_sprites
|
||||
update_sprites()
|
||||
else:
|
||||
logging.warning("Could not update LttP sprites.")
|
||||
|
||||
class ALTTPWorld(World):
|
||||
"""
|
||||
@@ -123,6 +142,8 @@ class ALTTPWorld(World):
|
||||
"""
|
||||
game = "A Link to the Past"
|
||||
option_definitions = alttp_options
|
||||
settings_key = "lttp_options"
|
||||
settings: typing.ClassVar[ALTTPSettings]
|
||||
topology_present = True
|
||||
item_name_groups = item_name_groups
|
||||
location_name_groups = {
|
||||
@@ -219,9 +240,16 @@ class ALTTPWorld(World):
|
||||
|
||||
create_items = generate_itempool
|
||||
|
||||
enemizer_path: str = Utils.get_options()["generator"]["enemizer_path"] \
|
||||
if os.path.isabs(Utils.get_options()["generator"]["enemizer_path"]) \
|
||||
else Utils.local_path(Utils.get_options()["generator"]["enemizer_path"])
|
||||
_enemizer_path: typing.ClassVar[typing.Optional[str]] = None
|
||||
|
||||
@property
|
||||
def enemizer_path(self) -> str:
|
||||
# TODO: directly use settings
|
||||
cls = self.__class__
|
||||
if cls._enemizer_path is None:
|
||||
cls._enemizer_path = settings.get_settings().generator.enemizer_path
|
||||
assert isinstance(cls._enemizer_path, str)
|
||||
return cls._enemizer_path
|
||||
|
||||
# custom instance vars
|
||||
dungeon_local_item_names: typing.Set[str]
|
||||
@@ -544,6 +572,44 @@ class ALTTPWorld(World):
|
||||
er_hint_data[region.player][location.address] = main_entrance.name
|
||||
hint_data.update(er_hint_data)
|
||||
|
||||
@classmethod
|
||||
def stage_modify_multidata(cls, multiworld, multidata: dict):
|
||||
|
||||
ordered_areas = (
|
||||
'Light World', 'Dark World', 'Hyrule Castle', 'Agahnims Tower', 'Eastern Palace', 'Desert Palace',
|
||||
'Tower of Hera', 'Palace of Darkness', 'Swamp Palace', 'Skull Woods', 'Thieves Town', 'Ice Palace',
|
||||
'Misery Mire', 'Turtle Rock', 'Ganons Tower', "Total"
|
||||
)
|
||||
|
||||
checks_in_area = {player: {area: list() for area in ordered_areas}
|
||||
for player in multiworld.get_game_players(cls.game)}
|
||||
|
||||
for player in checks_in_area:
|
||||
checks_in_area[player]["Total"] = 0
|
||||
|
||||
for location in multiworld.get_locations():
|
||||
if location.game == cls.game and type(location.address) is int:
|
||||
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)
|
||||
else:
|
||||
assert False, "Unknown Location area."
|
||||
# TODO: remove Total as it's duplicated data and breaks consistent typing
|
||||
checks_in_area[location.player]["Total"] += 1
|
||||
|
||||
multidata["checks_in_area"].update(checks_in_area)
|
||||
|
||||
def modify_multidata(self, multidata: dict):
|
||||
import base64
|
||||
# wait for self.rom_name to be available.
|
||||
@@ -724,6 +790,31 @@ class ALTTPWorld(World):
|
||||
res.append(item)
|
||||
return res
|
||||
|
||||
def fill_slot_data(self):
|
||||
slot_data = {}
|
||||
if not self.multiworld.is_race:
|
||||
# all of these option are NOT used by the SNI- or Text-Client.
|
||||
# they are used by the alttp-poptracker pack (https://github.com/StripesOO7/alttp-ap-poptracker-pack)
|
||||
# for convenient auto-tracking of the generated settings and adjusting the tracker accordingly
|
||||
|
||||
slot_options = ["crystals_needed_for_gt", "crystals_needed_for_ganon", "open_pyramid",
|
||||
"bigkey_shuffle", "smallkey_shuffle", "compass_shuffle", "map_shuffle",
|
||||
"progressive", "swordless", "retro_bow", "retro_caves", "shop_item_slots",
|
||||
"boss_shuffle", "pot_shuffle", "enemy_shuffle"]
|
||||
|
||||
slot_data = {option_name: getattr(self.multiworld, option_name)[self.player].value for option_name in slot_options}
|
||||
|
||||
slot_data.update({
|
||||
'mode': self.multiworld.mode[self.player],
|
||||
'goal': self.multiworld.goal[self.player],
|
||||
'dark_room_logic': self.multiworld.dark_room_logic[self.player],
|
||||
'mm_medalion': self.multiworld.required_medallions[self.player][0],
|
||||
'tr_medalion': self.multiworld.required_medallions[self.player][1],
|
||||
'shop_shuffle': self.multiworld.shop_shuffle[self.player],
|
||||
'entrance_shuffle': self.multiworld.shuffle[self.player]
|
||||
}
|
||||
)
|
||||
return slot_data
|
||||
|
||||
def get_same_seed(world, seed_def: tuple) -> str:
|
||||
seeds: typing.Dict[tuple, str] = getattr(world, "__named_seeds", {})
|
||||
|
||||
@@ -14,6 +14,7 @@ from worlds import AutoWorld
|
||||
class TestDungeon(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.multiworld = MultiWorld(1)
|
||||
self.multiworld.set_seed(None)
|
||||
args = Namespace()
|
||||
for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].option_definitions.items():
|
||||
setattr(args, name, {1: option.from_any(option.default)})
|
||||
|
||||
@@ -15,6 +15,7 @@ from worlds import AutoWorld
|
||||
class TestInverted(TestBase):
|
||||
def setUp(self):
|
||||
self.multiworld = MultiWorld(1)
|
||||
self.multiworld.set_seed(None)
|
||||
args = Namespace()
|
||||
for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].option_definitions.items():
|
||||
setattr(args, name, {1: option.from_any(option.default)})
|
||||
|
||||
@@ -15,6 +15,7 @@ class TestInvertedBombRules(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.multiworld = MultiWorld(1)
|
||||
self.multiworld.set_seed(None)
|
||||
self.multiworld.mode[1] = "inverted"
|
||||
args = Namespace
|
||||
for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].option_definitions.items():
|
||||
|
||||
@@ -16,6 +16,7 @@ from worlds import AutoWorld
|
||||
class TestInvertedMinor(TestBase):
|
||||
def setUp(self):
|
||||
self.multiworld = MultiWorld(1)
|
||||
self.multiworld.set_seed(None)
|
||||
args = Namespace()
|
||||
for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].option_definitions.items():
|
||||
setattr(args, name, {1: option.from_any(option.default)})
|
||||
|
||||
@@ -17,6 +17,7 @@ from worlds import AutoWorld
|
||||
class TestInvertedOWG(TestBase):
|
||||
def setUp(self):
|
||||
self.multiworld = MultiWorld(1)
|
||||
self.multiworld.set_seed(None)
|
||||
args = Namespace()
|
||||
for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].option_definitions.items():
|
||||
setattr(args, name, {1: option.from_any(option.default)})
|
||||
|
||||
@@ -16,6 +16,7 @@ from worlds import AutoWorld
|
||||
class TestMinor(TestBase):
|
||||
def setUp(self):
|
||||
self.multiworld = MultiWorld(1)
|
||||
self.multiworld.set_seed(None)
|
||||
args = Namespace()
|
||||
for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].option_definitions.items():
|
||||
setattr(args, name, {1: option.from_any(option.default)})
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
from argparse import Namespace
|
||||
|
||||
from BaseClasses import MultiWorld
|
||||
from worlds.alttp.Dungeons import create_dungeons, get_dungeon_item_pool
|
||||
from worlds.alttp.EntranceShuffle import link_entrances
|
||||
from worlds.alttp.Dungeons import get_dungeon_item_pool
|
||||
from worlds.alttp.InvertedRegions import mark_dark_world_regions
|
||||
from worlds.alttp.ItemPool import difficulties, generate_itempool
|
||||
from worlds.alttp.ItemPool import difficulties
|
||||
from worlds.alttp.Items import ItemFactory
|
||||
from worlds.alttp.Regions import create_regions
|
||||
from worlds.alttp.Shops import create_shops
|
||||
from worlds.alttp.Rules import set_rules
|
||||
from test.TestBase import TestBase
|
||||
|
||||
from worlds import AutoWorld
|
||||
@@ -17,6 +13,7 @@ from worlds import AutoWorld
|
||||
class TestVanillaOWG(TestBase):
|
||||
def setUp(self):
|
||||
self.multiworld = MultiWorld(1)
|
||||
self.multiworld.set_seed(None)
|
||||
args = Namespace()
|
||||
for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].option_definitions.items():
|
||||
setattr(args, name, {1: option.from_any(option.default)})
|
||||
|
||||
@@ -1,20 +1,17 @@
|
||||
from argparse import Namespace
|
||||
|
||||
from BaseClasses import MultiWorld
|
||||
from worlds.alttp.Dungeons import create_dungeons, get_dungeon_item_pool
|
||||
from worlds.alttp.EntranceShuffle import link_entrances
|
||||
from worlds.alttp.Dungeons import get_dungeon_item_pool
|
||||
from worlds.alttp.InvertedRegions import mark_dark_world_regions
|
||||
from worlds.alttp.ItemPool import difficulties, generate_itempool
|
||||
from worlds.alttp.ItemPool import difficulties
|
||||
from worlds.alttp.Items import ItemFactory
|
||||
from worlds.alttp.Regions import create_regions
|
||||
from worlds.alttp.Shops import create_shops
|
||||
from worlds.alttp.Rules import set_rules
|
||||
from test.TestBase import TestBase
|
||||
from worlds import AutoWorld
|
||||
|
||||
class TestVanilla(TestBase):
|
||||
def setUp(self):
|
||||
self.multiworld = MultiWorld(1)
|
||||
self.multiworld.set_seed(None)
|
||||
args = Namespace()
|
||||
for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].option_definitions.items():
|
||||
setattr(args, name, {1: option.from_any(option.default)})
|
||||
|
||||
@@ -1,135 +0,0 @@
|
||||
from typing import List, Dict
|
||||
|
||||
|
||||
region_exit_table: Dict[str, List[str]] = {
|
||||
"menu" : ["New Game"],
|
||||
|
||||
"albero" : ["To The Holy Line",
|
||||
"To Desecrated Cistern",
|
||||
"To Wasteland of the Buried Churches",
|
||||
"To Dungeons"],
|
||||
|
||||
"attots" : ["To Mother of Mothers"],
|
||||
|
||||
"ar" : ["To Mother of Mothers",
|
||||
"To Wall of the Holy Prohibitions",
|
||||
"To Deambulatory of His Holiness"],
|
||||
|
||||
"bottc" : ["To Wasteland of the Buried Churches",
|
||||
"To Ferrous Tree"],
|
||||
|
||||
"botss" : ["To The Holy Line",
|
||||
"To Mountains of the Endless Dusk"],
|
||||
|
||||
"coolotcv" : ["To Graveyard of the Peaks",
|
||||
"To Wall of the Holy Prohibitions"],
|
||||
|
||||
"dohh" : ["To Archcathedral Rooftops"],
|
||||
|
||||
"dc" : ["To Albero",
|
||||
"To Mercy Dreams",
|
||||
"To Mountains of the Endless Dusk",
|
||||
"To Echoes of Salt",
|
||||
"To Grievance Ascends"],
|
||||
|
||||
"eos" : ["To Jondo",
|
||||
"To Mountains of the Endless Dusk",
|
||||
"To Desecrated Cistern",
|
||||
"To The Resting Place of the Sister",
|
||||
"To Mourning and Havoc"],
|
||||
|
||||
"ft" : ["To Bridge of the Three Cavalries",
|
||||
"To Hall of the Dawning",
|
||||
"To Patio of the Silent Steps"],
|
||||
|
||||
"gotp" : ["To Where Olive Trees Wither",
|
||||
"To Convent of Our Lady of the Charred Visage"],
|
||||
|
||||
"ga" : ["To Jondo",
|
||||
"To Desecrated Cistern"],
|
||||
|
||||
"hotd" : ["To Ferrous Tree"],
|
||||
|
||||
"jondo" : ["To Mountains of the Endless Dusk",
|
||||
"To Grievance Ascends"],
|
||||
|
||||
"kottw" : ["To Mother of Mothers"],
|
||||
|
||||
"lotnw" : ["To Mother of Mothers",
|
||||
"To The Sleeping Canvases"],
|
||||
|
||||
"md" : ["To Wasteland of the Buried Churches",
|
||||
"To Desecrated Cistern",
|
||||
"To The Sleeping Canvases"],
|
||||
|
||||
"mom" : ["To Patio of the Silent Steps",
|
||||
"To Archcathedral Rooftops",
|
||||
"To Knot of the Three Words",
|
||||
"To Library of the Negated Words",
|
||||
"To All the Tears of the Sea"],
|
||||
|
||||
"moted" : ["To Brotherhood of the Silent Sorrow",
|
||||
"To Jondo",
|
||||
"To Desecrated Cistern"],
|
||||
|
||||
"mah" : ["To Echoes of Salt",
|
||||
"To Mother of Mothers"],
|
||||
|
||||
"potss" : ["To Ferrous Tree",
|
||||
"To Mother of Mothers",
|
||||
"To Wall of the Holy Prohibitions"],
|
||||
|
||||
"petrous" : ["To The Holy Line"],
|
||||
|
||||
"thl" : ["To Brotherhood of the Silent Sorrow",
|
||||
"To Petrous",
|
||||
"To Albero"],
|
||||
|
||||
"trpots" : ["To Echoes of Salt"],
|
||||
|
||||
"tsc" : ["To Library of the Negated Words",
|
||||
"To Mercy Dreams"],
|
||||
|
||||
"wothp" : ["To Archcathedral Rooftops",
|
||||
"To Convent of Our Lady of the Charred Visage"],
|
||||
|
||||
"wotbc" : ["To Albero",
|
||||
"To Where Olive Trees Wither",
|
||||
"To Mercy Dreams"],
|
||||
|
||||
"wotw" : ["To Wasteland of the Buried Churches",
|
||||
"To Graveyard of the Peaks"]
|
||||
}
|
||||
|
||||
exit_lookup_table: Dict[str, str] = {
|
||||
"New Game": "botss",
|
||||
"To Albero": "albero",
|
||||
"To All the Tears of the Sea": "attots",
|
||||
"To Archcathedral Rooftops": "ar",
|
||||
"To Bridge of the Three Cavalries": "bottc",
|
||||
"To Brotherhood of the Silent Sorrow": "botss",
|
||||
"To Convent of Our Lady of the Charred Visage": "coolotcv",
|
||||
"To Deambulatory of His Holiness": "dohh",
|
||||
"To Desecrated Cistern": "dc",
|
||||
"To Echoes of Salt": "eos",
|
||||
"To Ferrous Tree": "ft",
|
||||
"To Graveyard of the Peaks": "gotp",
|
||||
"To Grievance Ascends": "ga",
|
||||
"To Hall of the Dawning": "hotd",
|
||||
"To Jondo": "jondo",
|
||||
"To Knot of the Three Words": "kottw",
|
||||
"To Library of the Negated Words": "lotnw",
|
||||
"To Mercy Dreams": "md",
|
||||
"To Mother of Mothers": "mom",
|
||||
"To Mountains of the Endless Dusk": "moted",
|
||||
"To Mourning and Havoc": "mah",
|
||||
"To Patio of the Silent Steps": "potss",
|
||||
"To Petrous": "petrous",
|
||||
"To The Holy Line": "thl",
|
||||
"To The Resting Place of the Sister": "trpots",
|
||||
"To The Sleeping Canvases": "tsc",
|
||||
"To Wall of the Holy Prohibitions": "wothp",
|
||||
"To Wasteland of the Buried Churches": "wotbc",
|
||||
"To Where Olive Trees Wither": "wotw",
|
||||
"To Dungeons": "dungeon"
|
||||
}
|
||||
@@ -219,6 +219,12 @@ item_table: List[ItemDict] = [
|
||||
{'name': "Three Gnarled Tongues",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Boots of Pleading",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Purified Hand of the Nun",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.progression},
|
||||
|
||||
# Mea Culpa Hearts
|
||||
{'name': "Smoking Heart of Incense",
|
||||
@@ -372,7 +378,7 @@ item_table: List[ItemDict] = [
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Quicksilver",
|
||||
'count': 5,
|
||||
'classification': ItemClassification.useful},
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Petrified Bell",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.progression},
|
||||
@@ -398,7 +404,7 @@ item_table: List[ItemDict] = [
|
||||
# Skills
|
||||
{'name': "Combo Skill",
|
||||
'count': 3,
|
||||
'classification': ItemClassification.useful},
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Charged Skill",
|
||||
'count': 3,
|
||||
'classification': ItemClassification.progression},
|
||||
@@ -410,7 +416,13 @@ item_table: List[ItemDict] = [
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Lunge Skill",
|
||||
'count': 3,
|
||||
'classification': ItemClassification.useful},
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Dash Ability",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Wall Climb Ability",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.progression},
|
||||
|
||||
# Other
|
||||
{'name': "Parietal bone of Lasser, the Inquisitor",
|
||||
@@ -625,6 +637,23 @@ item_table: List[ItemDict] = [
|
||||
'classification': ItemClassification.filler}
|
||||
]
|
||||
|
||||
event_table: Dict[str, str] = {
|
||||
"OpenedDCGateW": "D01Z05S24",
|
||||
"OpenedDCGateE": "D01Z05S12",
|
||||
"OpenedDCLadder": "D01Z05S20",
|
||||
"OpenedWOTWCave": "D02Z01S06",
|
||||
"RodeGOTPElevator": "D02Z02S11",
|
||||
"OpenedConventLadder": "D02Z03S11",
|
||||
"BrokeJondoBellW": "D03Z02S09",
|
||||
"BrokeJondoBellE": "D03Z02S05",
|
||||
"OpenedMOMLadder": "D04Z02S06",
|
||||
"OpenedTSCGate": "D05Z02S11",
|
||||
"OpenedARLadder": "D06Z01S23",
|
||||
"BrokeBOTTCStatue": "D08Z01S02",
|
||||
"OpenedWOTHPGate": "D09Z01S05",
|
||||
"OpenedBOTSSLadder": "D17Z01S04"
|
||||
}
|
||||
|
||||
group_table: Dict[str, Set[str]] = {
|
||||
"wounds" : ["Holy Wound of Attrition",
|
||||
"Holy Wound of Contrition",
|
||||
@@ -634,6 +663,10 @@ group_table: Dict[str, Set[str]] = {
|
||||
"Mirrored Mask of Dolphos",
|
||||
"Embossed Mask of Crescente"],
|
||||
|
||||
"marks" : ["Mark of the First Refuge",
|
||||
"Mark of the Second Refuge",
|
||||
"Mark of the Third Refuge"],
|
||||
|
||||
"tirso" : ["Bouquet of Rosemary",
|
||||
"Incense Garlic",
|
||||
"Olive Seeds",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,22 @@
|
||||
from Options import Choice, Toggle, DefaultOnToggle, DeathLink
|
||||
from Options import Choice, Toggle, DefaultOnToggle, DeathLink, StartInventoryPool
|
||||
import random
|
||||
|
||||
|
||||
class ChoiceIsRandom(Choice):
|
||||
randomized: bool = False
|
||||
|
||||
@classmethod
|
||||
def from_text(cls, text: str) -> Choice:
|
||||
text = text.lower()
|
||||
if text == "random":
|
||||
cls.randomized = True
|
||||
return cls(random.choice(list(cls.name_lookup)))
|
||||
for option_name, value in cls.options.items():
|
||||
if option_name == text:
|
||||
return cls(value)
|
||||
raise KeyError(
|
||||
f'Could not find option "{text}" for "{cls.__name__}", '
|
||||
f'known options are {", ".join(f"{option}" for option in cls.name_lookup.values())}')
|
||||
|
||||
|
||||
class PrieDieuWarp(DefaultOnToggle):
|
||||
@@ -17,13 +35,12 @@ class CorpseHints(DefaultOnToggle):
|
||||
|
||||
|
||||
class Difficulty(Choice):
|
||||
"""Adjusts the logic required to defeat bosses.
|
||||
Impossible: Removes all logic requirements for bosses. Good luck."""
|
||||
"""Adjusts the overall difficulty of the randomizer, including upgrades required to defeat bosses
|
||||
and advanced movement tricks or glitches."""
|
||||
display_name = "Difficulty"
|
||||
option_easy = 0
|
||||
option_normal = 1
|
||||
option_hard = 2
|
||||
option_impossible = 3
|
||||
default = 1
|
||||
|
||||
|
||||
@@ -32,9 +49,20 @@ class Penitence(Toggle):
|
||||
display_name = "Penitence"
|
||||
|
||||
|
||||
class ExpertLogic(Toggle):
|
||||
"""Expands the logic used by the randomizer to allow for some difficult and/or lesser known tricks."""
|
||||
display_name = "Expert Logic"
|
||||
class StartingLocation(ChoiceIsRandom):
|
||||
"""Choose where to start the randomizer. Note that some starting locations cannot be chosen with certain
|
||||
other options.
|
||||
Specifically, Brotherhood and Mourning And Havoc cannot be chosen if Shuffle Dash is enabled, and Grievance Ascends
|
||||
cannot be chosen if Shuffle Wall Climb is enabled."""
|
||||
display_name = "Starting Location"
|
||||
option_brotherhood = 0
|
||||
option_albero = 1
|
||||
option_convent = 2
|
||||
option_grievance = 3
|
||||
option_knot_of_words = 4
|
||||
option_rooftops = 5
|
||||
option_mourning_havoc = 6
|
||||
default = 0
|
||||
|
||||
|
||||
class Ending(Choice):
|
||||
@@ -48,6 +76,13 @@ class Ending(Choice):
|
||||
default = 0
|
||||
|
||||
|
||||
class SkipLongQuests(Toggle):
|
||||
"""Ensures that the rewards for long quests will be filler items.
|
||||
Affected locations: \"Albero: Donate 50000 Tears\", \"Ossuary: 11th reward\", \"AtTotS: Miriam's gift\",
|
||||
\"TSC: Jocinero's final reward\""""
|
||||
display_name = "Skip Long Quests"
|
||||
|
||||
|
||||
class ThornShuffle(Choice):
|
||||
"""Shuffles the Thorn given by Deogracias and all Thorn upgrades into the item pool."""
|
||||
display_name = "Shuffle Thorn"
|
||||
@@ -57,124 +92,33 @@ class ThornShuffle(Choice):
|
||||
default = 0
|
||||
|
||||
|
||||
class DashShuffle(Toggle):
|
||||
"""Turns the ability to dash into an item that must be found in the multiworld."""
|
||||
display_name = "Shuffle Dash"
|
||||
|
||||
|
||||
class WallClimbShuffle(Toggle):
|
||||
"""Turns the ability to climb walls with your sword into an item that must be found in the multiworld."""
|
||||
display_name = "Shuffle Wall Climb"
|
||||
|
||||
|
||||
class ReliquaryShuffle(DefaultOnToggle):
|
||||
"""Adds the True Torment exclusive Reliquary rosary beads into the item pool."""
|
||||
display_name = "Shuffle Penitence Rewards"
|
||||
|
||||
|
||||
class CherubShuffle(DefaultOnToggle):
|
||||
"""Shuffles Children of Moonlight into the item pool."""
|
||||
display_name = "Shuffle Children of Moonlight"
|
||||
class CustomItem1(Toggle):
|
||||
"""Adds the custom relic Boots of Pleading into the item pool, which grants the ability to fall onto spikes
|
||||
and survive.
|
||||
Must have the \"Blasphemous-Boots-of-Pleading\" mod installed to connect to a multiworld."""
|
||||
display_name = "Boots of Pleading"
|
||||
|
||||
|
||||
class LifeShuffle(DefaultOnToggle):
|
||||
"""Shuffles life upgrades from the Lady of the Six Sorrows into the item pool."""
|
||||
display_name = "Shuffle Life Upgrades"
|
||||
|
||||
|
||||
class FervourShuffle(DefaultOnToggle):
|
||||
"""Shuffles fervour upgrades from the Oil of the Pilgrims into the item pool."""
|
||||
display_name = "Shuffle Fervour Upgrades"
|
||||
|
||||
|
||||
class SwordShuffle(DefaultOnToggle):
|
||||
"""Shuffles Mea Culpa upgrades from the Mea Culpa Altars into the item pool."""
|
||||
display_name = "Shuffle Mea Culpa Upgrades"
|
||||
|
||||
|
||||
class BlessingShuffle(DefaultOnToggle):
|
||||
"""Shuffles blessings from the Lake of Silent Pilgrims into the item pool."""
|
||||
display_name = "Shuffle Blessings"
|
||||
|
||||
|
||||
class DungeonShuffle(DefaultOnToggle):
|
||||
"""Shuffles rewards from completing Confessor Dungeons into the item pool."""
|
||||
display_name = "Shuffle Dungeon Rewards"
|
||||
|
||||
|
||||
class TirsoShuffle(DefaultOnToggle):
|
||||
"""Shuffles rewards from delivering herbs to Tirso into the item pool."""
|
||||
display_name = "Shuffle Tirso's Rewards"
|
||||
|
||||
|
||||
class MiriamShuffle(DefaultOnToggle):
|
||||
"""Shuffles the prayer given by Miriam into the item pool."""
|
||||
display_name = "Shuffle Miriram's Reward"
|
||||
|
||||
|
||||
class RedentoShuffle(DefaultOnToggle):
|
||||
"""Shuffles rewards from assisting Redento into the item pool."""
|
||||
display_name = "Shuffle Redento's Rewards"
|
||||
|
||||
|
||||
class JocineroShuffle(DefaultOnToggle):
|
||||
"""Shuffles rewards from rescuing 20 and 38 Children of Moonlight into the item pool."""
|
||||
display_name = "Shuffle Jocinero's Rewards"
|
||||
|
||||
|
||||
class AltasgraciasShuffle(DefaultOnToggle):
|
||||
"""Shuffles the reward given by Altasgracias and the item left behind by them into the item pool."""
|
||||
display_name = "Shuffle Altasgracias' Rewards"
|
||||
|
||||
|
||||
class TentudiaShuffle(DefaultOnToggle):
|
||||
"""Shuffles the rewards from delivering Tentudia's remains to Lvdovico into the item pool."""
|
||||
display_name = "Shuffle Lvdovico's Rewards"
|
||||
|
||||
|
||||
class GeminoShuffle(DefaultOnToggle):
|
||||
"""Shuffles the rewards from Gemino's quest and the hidden tomb into the item pool."""
|
||||
display_name = "Shuffle Gemino's Rewards"
|
||||
|
||||
|
||||
class GuiltShuffle(DefaultOnToggle):
|
||||
"""Shuffles the Weight of True Guilt into the item pool."""
|
||||
display_name = "Shuffle Immaculate Bead"
|
||||
|
||||
|
||||
class OssuaryShuffle(DefaultOnToggle):
|
||||
"""Shuffles the rewards from delivering bones to the Ossuary into the item pool."""
|
||||
display_name = "Shuffle Ossuary Rewards"
|
||||
|
||||
|
||||
class BossShuffle(DefaultOnToggle):
|
||||
"""Shuffles the Tears of Atonement from defeating bosses into the item pool."""
|
||||
display_name = "Shuffle Boss Tears"
|
||||
|
||||
|
||||
class WoundShuffle(DefaultOnToggle):
|
||||
"""Shuffles the Holy Wounds required to pass the Bridge of the Three Cavalries into the item pool."""
|
||||
display_name = "Shuffle Holy Wounds"
|
||||
|
||||
|
||||
class MaskShuffle(DefaultOnToggle):
|
||||
"""Shuffles the masks required to use the elevator in Archcathedral Rooftops into the item pool."""
|
||||
display_name = "Shuffle Masks"
|
||||
|
||||
|
||||
class EyeShuffle(DefaultOnToggle):
|
||||
"""Shuffles the Eyes of the Traitor from defeating Isidora and Sierpes into the item pool."""
|
||||
display_name = "Shuffle Traitor's Eyes"
|
||||
|
||||
|
||||
class HerbShuffle(DefaultOnToggle):
|
||||
"""Shuffles the herbs required for Tirso's quest into the item pool."""
|
||||
display_name = "Shuffle Herbs"
|
||||
|
||||
|
||||
class ChurchShuffle(DefaultOnToggle):
|
||||
"""Shuffles the rewards from donating 5,000 and 50,000 Tears of Atonement to the Church in Albero into the item pool."""
|
||||
display_name = "Shuffle Donation Rewards"
|
||||
|
||||
|
||||
class ShopShuffle(DefaultOnToggle):
|
||||
"""Shuffles the items sold in Candelaria's shops into the item pool."""
|
||||
display_name = "Shuffle Shop Items"
|
||||
|
||||
|
||||
class CandleShuffle(DefaultOnToggle):
|
||||
"""Shuffles the Beads of Wax and their upgrades into the item pool."""
|
||||
display_name = "Shuffle Candles"
|
||||
class CustomItem2(Toggle):
|
||||
"""Adds the custom relic Purified Hand of the Nun into the item pool, which grants the ability to jump
|
||||
a second time in mid-air.
|
||||
Must have the \"Blasphemous-Double-Jump\" mod installed to connect to a multiworld."""
|
||||
display_name = "Purified Hand of the Nun"
|
||||
|
||||
|
||||
class StartWheel(Toggle):
|
||||
@@ -189,7 +133,8 @@ class SkillRando(Toggle):
|
||||
|
||||
class EnemyRando(Choice):
|
||||
"""Randomizes the enemies that appear in each room.
|
||||
Shuffled: Enemies will be shuffled amongst each other, but can only appear as many times as they do in a standard game.
|
||||
Shuffled: Enemies will be shuffled amongst each other, but can only appear as many times as they do in
|
||||
a standard game.
|
||||
Randomized: Every enemy is completely random, and can appear any number of times.
|
||||
Some enemies will never be randomized."""
|
||||
display_name = "Enemy Randomizer"
|
||||
@@ -223,37 +168,20 @@ blasphemous_options = {
|
||||
"corpse_hints": CorpseHints,
|
||||
"difficulty": Difficulty,
|
||||
"penitence": Penitence,
|
||||
"expert_logic": ExpertLogic,
|
||||
"starting_location": StartingLocation,
|
||||
"ending": Ending,
|
||||
"skip_long_quests": SkipLongQuests,
|
||||
"thorn_shuffle" : ThornShuffle,
|
||||
"dash_shuffle": DashShuffle,
|
||||
"wall_climb_shuffle": WallClimbShuffle,
|
||||
"reliquary_shuffle": ReliquaryShuffle,
|
||||
"cherub_shuffle" : CherubShuffle,
|
||||
"life_shuffle" : LifeShuffle,
|
||||
"fervour_shuffle" : FervourShuffle,
|
||||
"sword_shuffle" : SwordShuffle,
|
||||
"blessing_shuffle" : BlessingShuffle,
|
||||
"dungeon_shuffle" : DungeonShuffle,
|
||||
"tirso_shuffle" : TirsoShuffle,
|
||||
"miriam_shuffle" : MiriamShuffle,
|
||||
"redento_shuffle" : RedentoShuffle,
|
||||
"jocinero_shuffle" : JocineroShuffle,
|
||||
"altasgracias_shuffle" : AltasgraciasShuffle,
|
||||
"tentudia_shuffle" : TentudiaShuffle,
|
||||
"gemino_shuffle" : GeminoShuffle,
|
||||
"guilt_shuffle" : GuiltShuffle,
|
||||
"ossuary_shuffle" : OssuaryShuffle,
|
||||
"boss_shuffle" : BossShuffle,
|
||||
"wound_shuffle" : WoundShuffle,
|
||||
"mask_shuffle" : MaskShuffle,
|
||||
"eye_shuffle": EyeShuffle,
|
||||
"herb_shuffle" : HerbShuffle,
|
||||
"church_shuffle" : ChurchShuffle,
|
||||
"shop_shuffle" : ShopShuffle,
|
||||
"candle_shuffle" : CandleShuffle,
|
||||
"boots_of_pleading": CustomItem1,
|
||||
"purified_hand": CustomItem2,
|
||||
"start_wheel": StartWheel,
|
||||
"skill_randomizer": SkillRando,
|
||||
"enemy_randomizer": EnemyRando,
|
||||
"enemy_groups": EnemyGroups,
|
||||
"enemy_scaling": EnemyScaling,
|
||||
"death_link": BlasphemousDeathLink
|
||||
"death_link": BlasphemousDeathLink,
|
||||
"start_inventory": StartInventoryPool
|
||||
}
|
||||
5405
worlds/blasphemous/Rooms.py
Normal file
5405
worlds/blasphemous/Rooms.py
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -7,205 +7,14 @@ unrandomized_dict: Dict[str, str] = {
|
||||
"DC: Chalice room": "Chalice of Inverted Verses"
|
||||
}
|
||||
|
||||
cherub_set: Set[str] = [
|
||||
"Albero: Child of Moonlight",
|
||||
"AR: Upper west shaft Child of Moonlight",
|
||||
"BotSS: Starting room Child of Moonlight",
|
||||
"DC: Child of Moonlight, above water",
|
||||
"DC: Upper east Child of Moonlight",
|
||||
"DC: Child of Moonlight, miasma room",
|
||||
"DC: Child of Moonlight, behind pillar",
|
||||
"DC: Top of elevator Child of Moonlight",
|
||||
"DC: Elevator shaft Child of Moonlight",
|
||||
"GotP: Shop cave Child of Moonlight",
|
||||
"GotP: Elevator shaft Child of Moonlight",
|
||||
"GotP: West shaft Child of Moonlight",
|
||||
"GotP: Center shaft Child of Moonlight",
|
||||
"GA: Miasma room Child of Moonlight",
|
||||
"GA: Blood bridge Child of Moonlight",
|
||||
"GA: Lower east Child of Moonlight",
|
||||
"Jondo: Upper east Child of Moonlight",
|
||||
"Jondo: Spike tunnel Child of Moonlight",
|
||||
"Jondo: Upper west Child of Moonlight",
|
||||
"LotNW: Platform room Child of Moonlight",
|
||||
"LotNW: Lowest west Child of Moonlight",
|
||||
"LotNW: Elevator Child of Moonlight",
|
||||
"MD: Second area Child of Moonlight",
|
||||
"MD: Cave Child of Moonlight",
|
||||
"MoM: Lower west Child of Moonlight",
|
||||
"MoM: Upper center Child of Moonlight",
|
||||
"MotED: Child of Moonlight, above chasm",
|
||||
"PotSS: First area Child of Moonlight",
|
||||
"PotSS: Third area Child of Moonlight",
|
||||
"THL: Child of Moonlight",
|
||||
"WotHP: Upper east room, top bronze cell",
|
||||
"WotHP: Upper west room, top silver cell",
|
||||
"WotHP: Lower east room, bottom silver cell",
|
||||
"WotHP: Outside Child of Moonlight",
|
||||
"WotBC: Outside Child of Moonlight",
|
||||
"WotBC: Cliffside Child of Moonlight",
|
||||
"WOTW: Underground Child of Moonlight",
|
||||
"WOTW: Upper east Child of Moonlight",
|
||||
|
||||
junk_locations: Set[str] = [
|
||||
"Albero: Donate 50000 Tears",
|
||||
"Ossuary: 11th reward",
|
||||
"AtTotS: Miriam's gift",
|
||||
"TSC: Jocinero's final reward"
|
||||
]
|
||||
|
||||
life_set: Set[str] = [
|
||||
"AR: Lady of the Six Sorrows",
|
||||
"CoOLotCV: Lady of the Six Sorrows",
|
||||
"DC: Lady of the Six Sorrows, from MD",
|
||||
"DC: Lady of the Six Sorrows, elevator shaft",
|
||||
"GotP: Lady of the Six Sorrows",
|
||||
"LotNW: Lady of the Six Sorrows"
|
||||
]
|
||||
|
||||
fervour_set: Set[str] = [
|
||||
"DC: Oil of the Pilgrims",
|
||||
"GotP: Oil of the Pilgrims",
|
||||
"GA: Oil of the Pilgrims",
|
||||
"LotNW: Oil of the Pilgrims",
|
||||
"MoM: Oil of the Pilgrims",
|
||||
"WotHP: Oil of the Pilgrims"
|
||||
]
|
||||
|
||||
sword_set: Set[str] = [
|
||||
"Albero: Mea Culpa altar",
|
||||
"AR: Mea Culpa altar",
|
||||
"BotSS: Mea Culpa altar",
|
||||
"CoOLotCV: Mea Culpa altar",
|
||||
"DC: Mea Culpa altar",
|
||||
"LotNW: Mea Culpa altar",
|
||||
"MoM: Mea Culpa altar"
|
||||
]
|
||||
|
||||
blessing_dict: Dict[str, str] = {
|
||||
"Albero: Bless Severed Hand": "Incorrupt Hand of the Fraternal Master",
|
||||
"Albero: Bless Linen Cloth": "Shroud of Dreamt Sins",
|
||||
"Albero: Bless Hatched Egg": "Three Gnarled Tongues"
|
||||
}
|
||||
|
||||
dungeon_dict: Dict[str, str] = {
|
||||
"Confessor Dungeon 1 extra": "Tears of Atonement (1000)",
|
||||
"Confessor Dungeon 2 extra": "Heart of the Single Tone",
|
||||
"Confessor Dungeon 3 extra": "Tears of Atonement (3000)",
|
||||
"Confessor Dungeon 4 extra": "Embers of a Broken Star",
|
||||
"Confessor Dungeon 5 extra": "Tears of Atonement (5000)",
|
||||
"Confessor Dungeon 6 extra": "Scaly Coin",
|
||||
"Confessor Dungeon 7 extra": "Seashell of the Inverted Spiral"
|
||||
}
|
||||
|
||||
tirso_dict: Dict[str, str] = {
|
||||
"Albero: Tirso's 1st reward": "Linen Cloth",
|
||||
"Albero: Tirso's 2nd reward": "Tears of Atonement (500)",
|
||||
"Albero: Tirso's 3rd reward": "Tears of Atonement (1000)",
|
||||
"Albero: Tirso's 4th reward": "Tears of Atonement (2000)",
|
||||
"Albero: Tirso's 5th reward": "Tears of Atonement (5000)",
|
||||
"Albero: Tirso's 6th reward": "Tears of Atonement (10000)",
|
||||
"Albero: Tirso's final reward": "Knot of Rosary Rope"
|
||||
}
|
||||
|
||||
redento_dict: Dict[str, str] = {
|
||||
"MoM: Redento's treasure": "Nail Uprooted from Dirt",
|
||||
"MoM: Final meeting with Redento": "Knot of Rosary Rope",
|
||||
"MotED: 1st meeting with Redento": "Fourth Toe made of Limestone",
|
||||
"PotSS: 4th meeting with Redento": "Big Toe made of Limestone",
|
||||
"WotBC: 3rd meeting with Redento": "Little Toe made of Limestone"
|
||||
}
|
||||
|
||||
jocinero_dict: Dict[str, str] = {
|
||||
"TSC: Jocinero's 1st reward": "Linen of Golden Thread",
|
||||
"TSC: Jocinero's final reward": "Campanillero to the Sons of the Aurora"
|
||||
}
|
||||
|
||||
altasgracias_dict: Dict[str, str] = {
|
||||
"GA: Altasgracias' gift": "Egg of Deformity",
|
||||
"GA: Empty giant egg": "Knot of Hair"
|
||||
}
|
||||
|
||||
tentudia_dict: Dict[str, str] = {
|
||||
"Albero: Lvdovico's 1st reward": "Tears of Atonement (500)",
|
||||
"Albero: Lvdovico's 2nd reward": "Tears of Atonement (1000)",
|
||||
"Albero: Lvdovico's 3rd reward": "Debla of the Lights"
|
||||
}
|
||||
|
||||
gemino_dict: Dict[str, str] = {
|
||||
"WOTW: Gift for the tomb": "Dried Flowers bathed in Tears",
|
||||
"WOTW: Underground tomb": "Saeta Dolorosa",
|
||||
"WOTW: Gemino's gift": "Empty Golden Thimble",
|
||||
"WOTW: Gemino's reward": "Frozen Olive"
|
||||
}
|
||||
|
||||
ossuary_dict: Dict[str, str] = {
|
||||
"Ossuary: 1st reward": "Tears of Atonement (250)",
|
||||
"Ossuary: 2nd reward": "Tears of Atonement (500)",
|
||||
"Ossuary: 3rd reward": "Tears of Atonement (750)",
|
||||
"Ossuary: 4th reward": "Tears of Atonement (1000)",
|
||||
"Ossuary: 5th reward": "Tears of Atonement (1250)",
|
||||
"Ossuary: 6th reward": "Tears of Atonement (1500)",
|
||||
"Ossuary: 7th reward": "Tears of Atonement (1750)",
|
||||
"Ossuary: 8th reward": "Tears of Atonement (2000)",
|
||||
"Ossuary: 9th reward": "Tears of Atonement (2500)",
|
||||
"Ossuary: 10th reward": "Tears of Atonement (3000)",
|
||||
"Ossuary: 11th reward": "Tears of Atonement (5000)",
|
||||
}
|
||||
|
||||
boss_dict: Dict[str, str] = {
|
||||
"BotTC: Esdras, of the Anointed Legion": "Tears of Atonement (4300)",
|
||||
"BotSS: Warden of the Silent Sorrow": "Tears of Atonement (300)",
|
||||
"CoOLotCV: Our Lady of the Charred Visage": "Tears of Atonement (2600)",
|
||||
"HotD: Laudes, the First of the Amanecidas": "Tears of Atonement (30000)",
|
||||
"GotP: Amanecida of the Bejeweled Arrow": "Tears of Atonement (18000)",
|
||||
"GA: Tres Angustias": "Tears of Atonement (2100)",
|
||||
"MD: Ten Piedad": "Tears of Atonement (625)",
|
||||
"MoM: Melquiades, The Exhumed Archbishop": "Tears of Atonement (5500)",
|
||||
"MotED: Amanecida of the Golden Blades": "Tears of Atonement (18000)",
|
||||
"MaH: Sierpes": "Tears of Atonement (5000)",
|
||||
"PotSS: Amanecida of the Chiselled Steel": "Tears of Atonement (18000)",
|
||||
"TSC: Exposito, Scion of Abjuration": "Tears of Atonement (9000)",
|
||||
"WotHP: Quirce, Returned By The Flames": "Tears of Atonement (11250)",
|
||||
"WotHP: Amanecida of the Molten Thorn": "Tears of Atonement (18000)"
|
||||
}
|
||||
|
||||
wound_dict: Dict[str, str] = {
|
||||
"CoOLotCV: Visage of Compunction": "Holy Wound of Compunction",
|
||||
"GA: Visage of Contrition": "Holy Wound of Contrition",
|
||||
"MD: Visage of Attrition": "Holy Wound of Attrition"
|
||||
}
|
||||
|
||||
mask_dict: Dict[str, str] = {
|
||||
"CoOLotCV: Mask room": "Mirrored Mask of Dolphos",
|
||||
"LotNW: Mask room": "Embossed Mask of Crescente",
|
||||
"MoM: Mask room": "Deformed Mask of Orestes"
|
||||
}
|
||||
|
||||
eye_dict: Dict[str, str] = {
|
||||
"Ossuary: Isidora, Voice of the Dead": "Severed Right Eye of the Traitor",
|
||||
"MaH: Sierpes' eye": "Broken Left Eye of the Traitor"
|
||||
}
|
||||
|
||||
herb_dict: Dict[str, str] = {
|
||||
"Albero: Gate of Travel room": "Bouquet of Thyme",
|
||||
"Jondo: Lower east bell trap": "Bouquet of Rosemary",
|
||||
"MotED: Blood platform alcove": "Dried Clove",
|
||||
"PotSS: Third area lower ledge": "Olive Seeds",
|
||||
"TSC: Painting ladder ledge": "Sooty Garlic",
|
||||
"WOTW: Entrance to tomb": "Incense Garlic"
|
||||
}
|
||||
|
||||
church_dict: Dict[str, str] = {
|
||||
"Albero: Donate 5000 Tears": "Token of Appreciation",
|
||||
"Albero: Donate 50000 Tears": "Cloistered Ruby"
|
||||
}
|
||||
|
||||
shop_dict: Dict[str, str] = {
|
||||
"GotP: Shop item 1": "Torn Bridal Ribbon",
|
||||
"GotP: Shop item 2": "Calcified Eye of Erudition",
|
||||
"GotP: Shop item 3": "Ember of the Holy Cremation",
|
||||
"MD: Shop item 1": "Key to the Chamber of the Eldest Brother",
|
||||
"MD: Shop item 2": "Hollow Pearl",
|
||||
"MD: Shop item 3": "Moss Preserved in Glass",
|
||||
"TSC: Shop item 1": "Wicker Knot",
|
||||
"TSC: Shop item 2": "Empty Bile Vessel",
|
||||
"TSC: Shop item 3": "Key of the Inquisitor"
|
||||
}
|
||||
|
||||
thorn_set: Set[str] = {
|
||||
"THL: Deogracias' gift",
|
||||
@@ -218,14 +27,6 @@ thorn_set: Set[str] = {
|
||||
"Confessor Dungeon 7 main",
|
||||
}
|
||||
|
||||
candle_dict: Dict[str, str] = {
|
||||
"CoOLotCV: Red candle": "Bead of Red Wax",
|
||||
"LotNW: Red candle": "Bead of Red Wax",
|
||||
"MD: Red candle": "Bead of Red Wax",
|
||||
"BotSS: Blue candle": "Bead of Blue Wax",
|
||||
"CoOLotCV: Blue candle": "Bead of Blue Wax",
|
||||
"MD: Blue candle": "Bead of Blue Wax"
|
||||
}
|
||||
|
||||
skill_dict: Dict[str, str] = {
|
||||
"Skill 1, Tier 1": "Combo Skill",
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
from typing import Dict, Set, List, Any
|
||||
from typing import Dict, List, Set, Any
|
||||
from collections import Counter
|
||||
from BaseClasses import Region, Entrance, Location, Item, Tutorial, ItemClassification
|
||||
from ..AutoWorld import World, WebWorld
|
||||
from .Items import base_id, item_table, group_table, tears_set, reliquary_set, skill_set
|
||||
from .Locations import location_table, shop_set
|
||||
from .Exits import region_exit_table, exit_lookup_table
|
||||
from worlds.AutoWorld import World, WebWorld
|
||||
from .Items import base_id, item_table, group_table, tears_set, reliquary_set, event_table
|
||||
from .Locations import location_table
|
||||
from .Rooms import room_table, door_table
|
||||
from .Rules import rules
|
||||
from worlds.generic.Rules import set_rule
|
||||
from worlds.generic.Rules import set_rule, add_rule
|
||||
from .Options import blasphemous_options
|
||||
from . import Vanilla
|
||||
from .Vanilla import unrandomized_dict, junk_locations, thorn_set, skill_dict
|
||||
|
||||
|
||||
class BlasphemousWeb(WebWorld):
|
||||
@@ -32,7 +32,7 @@ class BlasphemousWorld(World):
|
||||
|
||||
game: str = "Blasphemous"
|
||||
web = BlasphemousWeb()
|
||||
data_version = 1
|
||||
data_version = 2
|
||||
|
||||
item_name_to_id = {item["name"]: (base_id + index) for index, item in enumerate(item_table)}
|
||||
location_name_to_id = {loc["name"]: (base_id + index) for index, loc in enumerate(location_table)}
|
||||
@@ -41,9 +41,20 @@ class BlasphemousWorld(World):
|
||||
item_name_groups = group_table
|
||||
option_definitions = blasphemous_options
|
||||
|
||||
required_client_version = (0, 4, 2)
|
||||
|
||||
|
||||
def __init__(self, multiworld, player):
|
||||
super(BlasphemousWorld, self).__init__(multiworld, player)
|
||||
self.start_room: str = "D17Z01S01"
|
||||
self.door_connections: Dict[str, str] = {}
|
||||
|
||||
|
||||
def set_rules(self):
|
||||
rules(self)
|
||||
for door in door_table:
|
||||
add_rule(self.multiworld.get_location(door["Id"], self.player),
|
||||
lambda state: state.can_reach(self.get_connected_door(door["Id"])), self.player)
|
||||
|
||||
|
||||
def create_item(self, name: str) -> "BlasphemousItem":
|
||||
@@ -61,102 +72,134 @@ class BlasphemousWorld(World):
|
||||
return self.multiworld.random.choice(tears_set)
|
||||
|
||||
|
||||
def create_items(self):
|
||||
placed_items = []
|
||||
def generate_early(self):
|
||||
world = self.multiworld
|
||||
player = self.player
|
||||
|
||||
placed_items.extend(Vanilla.unrandomized_dict.values())
|
||||
if not world.starting_location[player].randomized:
|
||||
if world.starting_location[player].value == 6 and world.difficulty[player].value < 2:
|
||||
raise Exception(f"[Blasphemous - '{world.get_player_name(player)}'] {world.starting_location[player]}"
|
||||
" cannot be chosen if Difficulty is lower than Hard.")
|
||||
|
||||
if not self.multiworld.reliquary_shuffle[self.player]:
|
||||
placed_items.extend(reliquary_set)
|
||||
elif self.multiworld.reliquary_shuffle[self.player]:
|
||||
placed_items.append("Tears of Atonement (250)")
|
||||
placed_items.append("Tears of Atonement (300)")
|
||||
placed_items.append("Tears of Atonement (500)")
|
||||
if (world.starting_location[player].value == 0 or world.starting_location[player].value == 6) \
|
||||
and world.dash_shuffle[player]:
|
||||
raise Exception(f"[Blasphemous - '{world.get_player_name(player)}'] {world.starting_location[player]}"
|
||||
" cannot be chosen if Shuffle Dash is enabled.")
|
||||
|
||||
if world.starting_location[player].value == 3 and world.wall_climb_shuffle[player]:
|
||||
raise Exception(f"[Blasphemous - '{world.get_player_name(player)}'] {world.starting_location[player]}"
|
||||
" cannot be chosen if Shuffle Wall Climb is enabled.")
|
||||
else:
|
||||
locations: List[int] = [ 0, 1, 2, 3, 4, 5, 6 ]
|
||||
invalid: bool = False
|
||||
|
||||
if not self.multiworld.cherub_shuffle[self.player]:
|
||||
for i in range(38):
|
||||
placed_items.append("Child of Moonlight")
|
||||
if world.difficulty[player].value < 2:
|
||||
locations.remove(6)
|
||||
|
||||
if not self.multiworld.life_shuffle[self.player]:
|
||||
for i in range(6):
|
||||
placed_items.append("Life Upgrade")
|
||||
if world.dash_shuffle[player]:
|
||||
locations.remove(0)
|
||||
if 6 in locations:
|
||||
locations.remove(6)
|
||||
|
||||
if not self.multiworld.fervour_shuffle[self.player]:
|
||||
for i in range(6):
|
||||
placed_items.append("Fervour Upgrade")
|
||||
if world.wall_climb_shuffle[player]:
|
||||
locations.remove(3)
|
||||
|
||||
if not self.multiworld.sword_shuffle[self.player]:
|
||||
for i in range(7):
|
||||
placed_items.append("Mea Culpa Upgrade")
|
||||
if world.starting_location[player].value == 6 and world.difficulty[player].value < 2:
|
||||
invalid = True
|
||||
|
||||
if not self.multiworld.blessing_shuffle[self.player]:
|
||||
placed_items.extend(Vanilla.blessing_dict.values())
|
||||
if (world.starting_location[player].value == 0 or world.starting_location[player].value == 6) \
|
||||
and world.dash_shuffle[player]:
|
||||
invalid = True
|
||||
|
||||
if world.starting_location[player].value == 3 and world.wall_climb_shuffle[player]:
|
||||
invalid = True
|
||||
|
||||
if not self.multiworld.dungeon_shuffle[self.player]:
|
||||
placed_items.extend(Vanilla.dungeon_dict.values())
|
||||
|
||||
if not self.multiworld.tirso_shuffle[self.player]:
|
||||
placed_items.extend(Vanilla.tirso_dict.values())
|
||||
|
||||
if not self.multiworld.miriam_shuffle[self.player]:
|
||||
placed_items.append("Cantina of the Blue Rose")
|
||||
|
||||
if not self.multiworld.redento_shuffle[self.player]:
|
||||
placed_items.extend(Vanilla.redento_dict.values())
|
||||
|
||||
if not self.multiworld.jocinero_shuffle[self.player]:
|
||||
placed_items.extend(Vanilla.jocinero_dict.values())
|
||||
if invalid:
|
||||
world.starting_location[player].value = world.random.choice(locations)
|
||||
|
||||
|
||||
if not self.multiworld.altasgracias_shuffle[self.player]:
|
||||
placed_items.extend(Vanilla.altasgracias_dict.values())
|
||||
if not world.dash_shuffle[player]:
|
||||
world.push_precollected(self.create_item("Dash Ability"))
|
||||
|
||||
if not self.multiworld.tentudia_shuffle[self.player]:
|
||||
placed_items.extend(Vanilla.tentudia_dict.values())
|
||||
if not world.wall_climb_shuffle[player]:
|
||||
world.push_precollected(self.create_item("Wall Climb Ability"))
|
||||
|
||||
if not self.multiworld.gemino_shuffle[self.player]:
|
||||
placed_items.extend(Vanilla.gemino_dict.values())
|
||||
if world.skip_long_quests[player]:
|
||||
for loc in junk_locations:
|
||||
world.exclude_locations[player].value.add(loc)
|
||||
|
||||
if not self.multiworld.guilt_shuffle[self.player]:
|
||||
placed_items.append("Weight of True Guilt")
|
||||
start_rooms: Dict[int, str] = {
|
||||
0: "D17Z01S01",
|
||||
1: "D01Z02S01",
|
||||
2: "D02Z03S09",
|
||||
3: "D03Z03S11",
|
||||
4: "D04Z03S01",
|
||||
5: "D06Z01S09",
|
||||
6: "D20Z02S09"
|
||||
}
|
||||
|
||||
if not self.multiworld.ossuary_shuffle[self.player]:
|
||||
placed_items.extend(Vanilla.ossuary_dict.values())
|
||||
self.start_room = start_rooms[world.starting_location[player].value]
|
||||
|
||||
if not self.multiworld.boss_shuffle[self.player]:
|
||||
placed_items.extend(Vanilla.boss_dict.values())
|
||||
|
||||
if not self.multiworld.wound_shuffle[self.player]:
|
||||
placed_items.extend(Vanilla.wound_dict.values())
|
||||
def create_items(self):
|
||||
world = self.multiworld
|
||||
player = self.player
|
||||
|
||||
if not self.multiworld.mask_shuffle[self.player]:
|
||||
placed_items.extend(Vanilla.mask_dict.values())
|
||||
removed: int = 0
|
||||
to_remove: List[str] = [
|
||||
"Tears of Atonement (250)",
|
||||
"Tears of Atonement (300)",
|
||||
"Tears of Atonement (500)",
|
||||
"Tears of Atonement (500)",
|
||||
"Tears of Atonement (500)"
|
||||
]
|
||||
|
||||
if not self.multiworld.eye_shuffle[self.player]:
|
||||
placed_items.extend(Vanilla.eye_dict.values())
|
||||
skipped_items = []
|
||||
junk: int = 0
|
||||
|
||||
if not self.multiworld.herb_shuffle[self.player]:
|
||||
placed_items.extend(Vanilla.herb_dict.values())
|
||||
for item, count in world.start_inventory[player].value.items():
|
||||
for _ in range(count):
|
||||
skipped_items.append(item)
|
||||
junk += 1
|
||||
|
||||
if not self.multiworld.church_shuffle[self.player]:
|
||||
placed_items.extend(Vanilla.church_dict.values())
|
||||
skipped_items.extend(unrandomized_dict.values())
|
||||
|
||||
if not self.multiworld.shop_shuffle[self.player]:
|
||||
placed_items.extend(Vanilla.shop_dict.values())
|
||||
|
||||
if self.multiworld.thorn_shuffle[self.player] == 2:
|
||||
if world.thorn_shuffle[player] == 2:
|
||||
for i in range(8):
|
||||
placed_items.append("Thorn Upgrade")
|
||||
skipped_items.append("Thorn Upgrade")
|
||||
|
||||
if not self.multiworld.candle_shuffle[self.player]:
|
||||
placed_items.extend(Vanilla.candle_dict.values())
|
||||
if world.dash_shuffle[player]:
|
||||
skipped_items.append(to_remove[removed])
|
||||
removed += 1
|
||||
elif not world.dash_shuffle[player]:
|
||||
skipped_items.append("Dash Ability")
|
||||
|
||||
if self.multiworld.start_wheel[self.player]:
|
||||
placed_items.append("The Young Mason's Wheel")
|
||||
if world.wall_climb_shuffle[player]:
|
||||
skipped_items.append(to_remove[removed])
|
||||
removed += 1
|
||||
elif not world.wall_climb_shuffle[player]:
|
||||
skipped_items.append("Wall Climb Ability")
|
||||
|
||||
if not self.multiworld.skill_randomizer[self.player]:
|
||||
placed_items.extend(Vanilla.skill_dict.values())
|
||||
if not world.reliquary_shuffle[player]:
|
||||
skipped_items.extend(reliquary_set)
|
||||
elif world.reliquary_shuffle[player]:
|
||||
for i in range(3):
|
||||
skipped_items.append(to_remove[removed])
|
||||
removed += 1
|
||||
|
||||
counter = Counter(placed_items)
|
||||
if not world.boots_of_pleading[player]:
|
||||
skipped_items.append("Boots of Pleading")
|
||||
|
||||
if not world.purified_hand[player]:
|
||||
skipped_items.append("Purified Hand of the Nun")
|
||||
|
||||
if world.start_wheel[player]:
|
||||
skipped_items.append("The Young Mason's Wheel")
|
||||
|
||||
if not world.skill_randomizer[player]:
|
||||
skipped_items.extend(skill_dict.values())
|
||||
|
||||
counter = Counter(skipped_items)
|
||||
|
||||
pool = []
|
||||
|
||||
@@ -169,95 +212,30 @@ class BlasphemousWorld(World):
|
||||
for i in range(count):
|
||||
pool.append(self.create_item(item["name"]))
|
||||
|
||||
self.multiworld.itempool += pool
|
||||
for _ in range(junk):
|
||||
pool.append(self.create_item(self.get_filler_item_name()))
|
||||
|
||||
world.itempool += pool
|
||||
|
||||
|
||||
def pre_fill(self):
|
||||
self.place_items_from_dict(Vanilla.unrandomized_dict)
|
||||
world = self.multiworld
|
||||
player = self.player
|
||||
|
||||
if not self.multiworld.cherub_shuffle[self.player]:
|
||||
self.place_items_from_set(Vanilla.cherub_set, "Child of Moonlight")
|
||||
self.place_items_from_dict(unrandomized_dict)
|
||||
|
||||
if not self.multiworld.life_shuffle[self.player]:
|
||||
self.place_items_from_set(Vanilla.life_set, "Life Upgrade")
|
||||
if world.thorn_shuffle[player] == 2:
|
||||
self.place_items_from_set(thorn_set, "Thorn Upgrade")
|
||||
|
||||
if not self.multiworld.fervour_shuffle[self.player]:
|
||||
self.place_items_from_set(Vanilla.fervour_set, "Fervour Upgrade")
|
||||
|
||||
if not self.multiworld.sword_shuffle[self.player]:
|
||||
self.place_items_from_set(Vanilla.sword_set, "Mea Culpa Upgrade")
|
||||
|
||||
if not self.multiworld.blessing_shuffle[self.player]:
|
||||
self.place_items_from_dict(Vanilla.blessing_dict)
|
||||
|
||||
if not self.multiworld.dungeon_shuffle[self.player]:
|
||||
self.place_items_from_dict(Vanilla.dungeon_dict)
|
||||
|
||||
if not self.multiworld.tirso_shuffle[self.player]:
|
||||
self.place_items_from_dict(Vanilla.tirso_dict)
|
||||
|
||||
if not self.multiworld.miriam_shuffle[self.player]:
|
||||
self.multiworld.get_location("AtTotS: Miriam's gift", self.player)\
|
||||
.place_locked_item(self.create_item("Cantina of the Blue Rose"))
|
||||
|
||||
if not self.multiworld.redento_shuffle[self.player]:
|
||||
self.place_items_from_dict(Vanilla.redento_dict)
|
||||
|
||||
if not self.multiworld.jocinero_shuffle[self.player]:
|
||||
self.place_items_from_dict(Vanilla.jocinero_dict)
|
||||
|
||||
if not self.multiworld.altasgracias_shuffle[self.player]:
|
||||
self.place_items_from_dict(Vanilla.altasgracias_dict)
|
||||
|
||||
if not self.multiworld.tentudia_shuffle[self.player]:
|
||||
self.place_items_from_dict(Vanilla.tentudia_dict)
|
||||
|
||||
if not self.multiworld.gemino_shuffle[self.player]:
|
||||
self.place_items_from_dict(Vanilla.gemino_dict)
|
||||
|
||||
if not self.multiworld.guilt_shuffle[self.player]:
|
||||
self.multiworld.get_location("GotP: Confessor Dungeon room", self.player)\
|
||||
.place_locked_item(self.create_item("Weight of True Guilt"))
|
||||
|
||||
if not self.multiworld.ossuary_shuffle[self.player]:
|
||||
self.place_items_from_dict(Vanilla.ossuary_dict)
|
||||
|
||||
if not self.multiworld.boss_shuffle[self.player]:
|
||||
self.place_items_from_dict(Vanilla.boss_dict)
|
||||
|
||||
if not self.multiworld.wound_shuffle[self.player]:
|
||||
self.place_items_from_dict(Vanilla.wound_dict)
|
||||
|
||||
if not self.multiworld.mask_shuffle[self.player]:
|
||||
self.place_items_from_dict(Vanilla.mask_dict)
|
||||
|
||||
if not self.multiworld.eye_shuffle[self.player]:
|
||||
self.place_items_from_dict(Vanilla.eye_dict)
|
||||
|
||||
if not self.multiworld.herb_shuffle[self.player]:
|
||||
self.place_items_from_dict(Vanilla.herb_dict)
|
||||
|
||||
if not self.multiworld.church_shuffle[self.player]:
|
||||
self.place_items_from_dict(Vanilla.church_dict)
|
||||
|
||||
if not self.multiworld.shop_shuffle[self.player]:
|
||||
self.place_items_from_dict(Vanilla.shop_dict)
|
||||
|
||||
if self.multiworld.thorn_shuffle[self.player] == 2:
|
||||
self.place_items_from_set(Vanilla.thorn_set, "Thorn Upgrade")
|
||||
|
||||
if not self.multiworld.candle_shuffle[self.player]:
|
||||
self.place_items_from_dict(Vanilla.candle_dict)
|
||||
|
||||
if self.multiworld.start_wheel[self.player]:
|
||||
self.multiworld.get_location("BotSS: Beginning gift", self.player)\
|
||||
if world.start_wheel[player]:
|
||||
world.get_location("Beginning gift", player)\
|
||||
.place_locked_item(self.create_item("The Young Mason's Wheel"))
|
||||
|
||||
if not self.multiworld.skill_randomizer[self.player]:
|
||||
self.place_items_from_dict(Vanilla.skill_dict)
|
||||
if not world.skill_randomizer[player]:
|
||||
self.place_items_from_dict(skill_dict)
|
||||
|
||||
if self.multiworld.thorn_shuffle[self.player] == 1:
|
||||
self.multiworld.local_items[self.player].value.add("Thorn Upgrade")
|
||||
if world.thorn_shuffle[player] == 1:
|
||||
world.local_items[player].value.add("Thorn Upgrade")
|
||||
|
||||
|
||||
def place_items_from_set(self, location_set: Set[str], name: str):
|
||||
@@ -273,133 +251,142 @@ class BlasphemousWorld(World):
|
||||
|
||||
|
||||
def create_regions(self) -> None:
|
||||
|
||||
player = self.player
|
||||
world = self.multiworld
|
||||
|
||||
region_table: Dict[str, Region] = {
|
||||
"menu" : Region("Menu", player, world),
|
||||
"albero" : Region("Albero", player, world),
|
||||
"attots" : Region("All the Tears of the Sea", player, world),
|
||||
"ar" : Region("Archcathedral Rooftops", player, world),
|
||||
"bottc" : Region("Bridge of the Three Cavalries", player, world),
|
||||
"botss" : Region("Brotherhood of the Silent Sorrow", player, world),
|
||||
"coolotcv": Region("Convent of Our Lady of the Charred Visage", player, world),
|
||||
"dohh" : Region("Deambulatory of His Holiness", player, world),
|
||||
"dc" : Region("Desecrated Cistern", player, world),
|
||||
"eos" : Region("Echoes of Salt", player, world),
|
||||
"ft" : Region("Ferrous Tree", player, world),
|
||||
"gotp" : Region("Graveyard of the Peaks", player, world),
|
||||
"ga" : Region("Grievance Ascends", player, world),
|
||||
"hotd" : Region("Hall of the Dawning", player, world),
|
||||
"jondo" : Region("Jondo", player, world),
|
||||
"kottw" : Region("Knot of the Three Words", player, world),
|
||||
"lotnw" : Region("Library of the Negated Words", player, world),
|
||||
"md" : Region("Mercy Dreams", player, world),
|
||||
"mom" : Region("Mother of Mothers", player, world),
|
||||
"moted" : Region("Mountains of the Endless Dusk", player, world),
|
||||
"mah" : Region("Mourning and Havoc", player, world),
|
||||
"potss" : Region("Patio of the Silent Steps", player, world),
|
||||
"petrous" : Region("Petrous", player, world),
|
||||
"thl" : Region("The Holy Line", player, world),
|
||||
"trpots" : Region("The Resting Place of the Sister", player, world),
|
||||
"tsc" : Region("The Sleeping Canvases", player, world),
|
||||
"wothp" : Region("Wall of the Holy Prohibitions", player, world),
|
||||
"wotbc" : Region("Wasteland of the Buried Churches", player, world),
|
||||
"wotw" : Region("Where Olive Trees Wither", player, world),
|
||||
"dungeon" : Region("Dungeons", player, world)
|
||||
}
|
||||
|
||||
for rname, reg in region_table.items():
|
||||
world.regions.append(reg)
|
||||
|
||||
for ename, exits in region_exit_table.items():
|
||||
if ename == rname:
|
||||
for i in exits:
|
||||
ent = Entrance(player, i, reg)
|
||||
reg.exits.append(ent)
|
||||
|
||||
for e, r in exit_lookup_table.items():
|
||||
if i == e:
|
||||
ent.connect(region_table[r])
|
||||
|
||||
for loc in location_table:
|
||||
id = base_id + location_table.index(loc)
|
||||
region_table[loc["region"]].locations\
|
||||
.append(BlasphemousLocation(self.player, loc["name"], id, region_table[loc["region"]]))
|
||||
|
||||
victory = Location(self.player, "His Holiness Escribar", None, self.multiworld.get_region("Deambulatory of His Holiness", self.player))
|
||||
victory.place_locked_item(self.create_event("Victory"))
|
||||
self.multiworld.get_region("Deambulatory of His Holiness", self.player).locations.append(victory)
|
||||
menu_region = Region("Menu", player, world)
|
||||
misc_region = Region("Misc", player, world)
|
||||
world.regions += [menu_region, misc_region]
|
||||
|
||||
if self.multiworld.ending[self.player].value == 1:
|
||||
for room in room_table:
|
||||
region = Region(room, player, world)
|
||||
world.regions.append(region)
|
||||
|
||||
menu_region.add_exits({self.start_room: "New Game"})
|
||||
world.get_region(self.start_room, player).add_exits({"Misc": "Misc"})
|
||||
|
||||
for door in door_table:
|
||||
if door.get("OriginalDoor") is None:
|
||||
continue
|
||||
else:
|
||||
if not door["Id"] in self.door_connections.keys():
|
||||
self.door_connections[door["Id"]] = door["OriginalDoor"]
|
||||
self.door_connections[door["OriginalDoor"]] = door["Id"]
|
||||
|
||||
parent_region: Region = self.get_room_from_door(door["Id"])
|
||||
target_region: Region = self.get_room_from_door(door["OriginalDoor"])
|
||||
parent_region.add_exits({
|
||||
target_region.name: door["Id"]
|
||||
}, {
|
||||
target_region.name: lambda x: door.get("VisibilityFlags") != 1
|
||||
})
|
||||
|
||||
for index, loc in enumerate(location_table):
|
||||
if not world.boots_of_pleading[player] and loc["name"] == "BotSS: 2nd meeting with Redento":
|
||||
continue
|
||||
if not world.purified_hand[player] and loc["name"] == "MoM: Western room ledge":
|
||||
continue
|
||||
|
||||
region: Region = world.get_region(loc["room"], player)
|
||||
region.add_locations({loc["name"]: base_id + index})
|
||||
#id = base_id + location_table.index(loc)
|
||||
#reg.locations.append(BlasphemousLocation(player, loc["name"], id, reg))
|
||||
|
||||
for e, r in event_table.items():
|
||||
region: Region = world.get_region(r, player)
|
||||
event = BlasphemousLocation(player, e, None, region)
|
||||
event.show_in_spoiler = False
|
||||
event.place_locked_item(self.create_event(e))
|
||||
region.locations.append(event)
|
||||
|
||||
for door in door_table:
|
||||
region: Region = self.get_room_from_door(self.door_connections[door["Id"]])
|
||||
event = BlasphemousLocation(player, door["Id"], None, region)
|
||||
event.show_in_spoiler = False
|
||||
event.place_locked_item(self.create_event(door["Id"]))
|
||||
region.locations.append(event)
|
||||
|
||||
victory = Location(player, "His Holiness Escribar", None, world.get_region("D07Z01S03", player))
|
||||
victory.place_locked_item(self.create_event("Victory"))
|
||||
world.get_region("D07Z01S03", player).locations.append(victory)
|
||||
|
||||
if world.ending[self.player].value == 1:
|
||||
set_rule(victory, lambda state: state.has("Thorn Upgrade", player, 8))
|
||||
elif self.multiworld.ending[self.player].value == 2:
|
||||
set_rule(victory, lambda state: state.has("Thorn Upgrade", player, 8) and \
|
||||
elif world.ending[self.player].value == 2:
|
||||
set_rule(victory, lambda state: state.has("Thorn Upgrade", player, 8) and
|
||||
state.has("Holy Wound of Abnegation", player))
|
||||
|
||||
self.multiworld.completion_condition[self.player] = lambda state: state.has("Victory", self.player)
|
||||
world.completion_condition[self.player] = lambda state: state.has("Victory", player)
|
||||
|
||||
|
||||
def get_room_from_door(self, door: str) -> Region:
|
||||
return self.multiworld.get_region(door.split("[")[0], self.player)
|
||||
|
||||
|
||||
def get_connected_door(self, door: str) -> Entrance:
|
||||
return self.multiworld.get_entrance(self.door_connections[door], self.player)
|
||||
|
||||
|
||||
def fill_slot_data(self) -> Dict[str, Any]:
|
||||
slot_data: Dict[str, Any] = {}
|
||||
locations = []
|
||||
doors: Dict[str, str] = {}
|
||||
|
||||
for loc in self.multiworld.get_filled_locations(self.player):
|
||||
if loc.name == "His Holiness Escribar":
|
||||
world = self.multiworld
|
||||
player = self.player
|
||||
thorns: bool = True
|
||||
|
||||
if world.thorn_shuffle[player].value == 2:
|
||||
thorns = False
|
||||
|
||||
for loc in world.get_filled_locations(player):
|
||||
if loc.item.code == None:
|
||||
continue
|
||||
else:
|
||||
data = {
|
||||
"id": self.location_name_to_game_id[loc.name],
|
||||
"ap_id": loc.address,
|
||||
"name": loc.item.name,
|
||||
"player_name": self.multiworld.player_name[loc.item.player]
|
||||
"player_name": world.player_name[loc.item.player],
|
||||
"type": int(loc.item.classification)
|
||||
}
|
||||
|
||||
if loc.name in shop_set:
|
||||
data["type"] = loc.item.classification.name
|
||||
|
||||
locations.append(data)
|
||||
|
||||
config = {
|
||||
"versionCreated": "AP",
|
||||
"general": {
|
||||
"teleportationAlwaysUnlocked": bool(self.multiworld.prie_dieu_warp[self.player].value),
|
||||
"skipCutscenes": bool(self.multiworld.skip_cutscenes[self.player].value),
|
||||
"enablePenitence": bool(self.multiworld.penitence[self.player].value),
|
||||
"hardMode": False,
|
||||
"customSeed": 0,
|
||||
"allowHints": bool(self.multiworld.corpse_hints[self.player].value)
|
||||
},
|
||||
"items": {
|
||||
"type": 1,
|
||||
"lungDamage": False,
|
||||
"disableNPCDeath": True,
|
||||
"startWithWheel": bool(self.multiworld.start_wheel[self.player].value),
|
||||
"shuffleReliquaries": bool(self.multiworld.reliquary_shuffle[self.player].value)
|
||||
},
|
||||
"enemies": {
|
||||
"type": self.multiworld.enemy_randomizer[self.player].value,
|
||||
"maintainClass": bool(self.multiworld.enemy_groups[self.player].value),
|
||||
"areaScaling": bool(self.multiworld.enemy_scaling[self.player].value)
|
||||
},
|
||||
"prayers": {
|
||||
"type": 0,
|
||||
"removeMirabis": False
|
||||
},
|
||||
"doors": {
|
||||
"type": 0
|
||||
},
|
||||
"debug": {
|
||||
"type": 0
|
||||
}
|
||||
"LogicDifficulty": world.difficulty[player].value,
|
||||
"StartingLocation": world.starting_location[player].value,
|
||||
"VersionCreated": "AP",
|
||||
|
||||
"UnlockTeleportation": bool(world.prie_dieu_warp[player].value),
|
||||
"AllowHints": bool(world.corpse_hints[player].value),
|
||||
"AllowPenitence": bool(world.penitence[player].value),
|
||||
|
||||
"ShuffleReliquaries": bool(world.reliquary_shuffle[player].value),
|
||||
"ShuffleBootsOfPleading": bool(world.boots_of_pleading[player].value),
|
||||
"ShufflePurifiedHand": bool(world.purified_hand[player].value),
|
||||
"ShuffleDash": bool(world.dash_shuffle[player].value),
|
||||
"ShuffleWallClimb": bool(world.wall_climb_shuffle[player].value),
|
||||
|
||||
"ShuffleSwordSkills": bool(world.skill_randomizer[player].value),
|
||||
"ShuffleThorns": thorns,
|
||||
"JunkLongQuests": bool(world.skip_long_quests[player].value),
|
||||
"StartWithWheel": bool(world.start_wheel[player].value),
|
||||
|
||||
"EnemyShuffleType": world.enemy_randomizer[player].value,
|
||||
"MaintainClass": bool(world.enemy_groups[player].value),
|
||||
"AreaScaling": bool(world.enemy_scaling[player].value),
|
||||
|
||||
"BossShuffleType": 0,
|
||||
"DoorShuffleType": 0
|
||||
}
|
||||
|
||||
slot_data = {
|
||||
"locations": locations,
|
||||
"doors": doors,
|
||||
"cfg": config,
|
||||
"ending": self.multiworld.ending[self.player].value,
|
||||
"death_link": bool(self.multiworld.death_link[self.player].value)
|
||||
"ending": world.ending[self.player].value,
|
||||
"death_link": bool(world.death_link[self.player].value)
|
||||
}
|
||||
|
||||
return slot_data
|
||||
|
||||
@@ -1,24 +1,49 @@
|
||||
# Blasphemous Multiworld Setup Guide
|
||||
|
||||
## Required Software
|
||||
## Useful Links
|
||||
|
||||
- Blasphemous from: [Steam](https://store.steampowered.com/app/774361/Blasphemous/)
|
||||
- Blasphemous Modding API from: [GitHub](https://github.com/BrandenEK/Blasphemous-Modding-API)
|
||||
- Blasphemous Randomizer from: [GitHub](https://github.com/BrandenEK/Blasphemous-Randomizer)
|
||||
- Blasphemous Multiworld from: [GitHub](https://github.com/BrandenEK/Blasphemous-Multiworld)
|
||||
- (*Optional*) PopTracker Pack from: [GitHub](https://github.com/sassyvania/Blasphemous-Randomizer-Maptracker)
|
||||
Required:
|
||||
- Blasphemous: [Steam](https://store.steampowered.com/app/774361/Blasphemous/)
|
||||
- The GOG version of Blasphemous will also work.
|
||||
- Blasphemous Mod Installer: [GitHub](https://github.com/BrandenEK/Blasphemous-Mod-Installer)
|
||||
- Blasphemous Modding API: [GitHub](https://github.com/BrandenEK/Blasphemous-Modding-API)
|
||||
- Blasphemous Randomizer: [GitHub](https://github.com/BrandenEK/Blasphemous-Randomizer)
|
||||
- Blasphemous Multiworld: [GitHub](https://github.com/BrandenEK/Blasphemous-Multiworld)
|
||||
|
||||
## Instructions (Windows)
|
||||
Optional:
|
||||
- In-game map tracker: [GitHub](https://github.com/BrandenEK/Blasphemous-Rando-Map)
|
||||
- Quick Prie Dieu warp mod: [GitHub](https://github.com/BadMagic100/Blasphemous-PrieWarp)
|
||||
- Boots of Pleading mod: [GitHub](https://github.com/BrandenEK/Blasphemous-Boots-of-Pleading)
|
||||
- Double Jump mod: [GitHub](https://github.com/BrandenEK/Blasphemous-Double-Jump)
|
||||
- PopTracker pack: [GitHub](https://github.com/sassyvania/Blasphemous-Randomizer-Maptracker)
|
||||
|
||||
1. Download the [Modding API](https://github.com/BrandenEK/Blasphemous-Modding-API/releases), and follow the [installation instructions](https://github.com/BrandenEK/Blasphemous-Modding-API#installation) on the GitHub page.
|
||||
## Mod Installer (Recommended)
|
||||
|
||||
2. After the Modding API has been installed, download the [Randomizer](https://github.com/BrandenEK/Blasphemous-Randomizer/releases) and [Multiworld](https://github.com/BrandenEK/Blasphemous-Multiworld/releases) archives, and extract the contents of both into the `Modding` folder.
|
||||
1. Download the [Mod Installer](https://github.com/BrandenEK/Blasphemous-Mod-Installer),
|
||||
and point it to your install directory for Blasphemous.
|
||||
|
||||
3. Start Blasphemous. To verfy that the mods are working, look for a version number for both the Randomizer and Multiworld on the title screen.
|
||||
2. Install the `Modding API`, `Randomizer`, and `Multiworld` mods. Optionally, you can also install the
|
||||
`Rando Map`, `PrieWarp`, `Boots of Pleading`, and `Double Jump` mods, and set up the PopTracker pack if desired.
|
||||
|
||||
4. (*Optional*) Add the Blasphemous pack to PopTracker. In game, open the console by pressing backslash `\` and type `randomizer autotracker on` to automatically connect the game to PopTracker.
|
||||
3. Start Blasphemous. To verfy that the mods are working, look for a version number for both
|
||||
the Randomizer and Multiworld on the title screen.
|
||||
|
||||
## Manual Installation
|
||||
|
||||
1. Download the [Modding API](https://github.com/BrandenEK/Blasphemous-Modding-API/releases), and follow
|
||||
the [installation instructions](https://github.com/BrandenEK/Blasphemous-Modding-API#installation) on the GitHub page.
|
||||
|
||||
2. After the Modding API has been installed, download the
|
||||
[Randomizer](https://github.com/BrandenEK/Blasphemous-Randomizer/releases) and
|
||||
[Multiworld](https://github.com/BrandenEK/Blasphemous-Multiworld/releases) archives, and extract the contents of both
|
||||
into the `Modding` folder. Then, add any desired additional mods.
|
||||
|
||||
3. Start Blasphemous. To verfy that the mods are working, look for a version number for both
|
||||
the Randomizer and Multiworld on the title screen.
|
||||
|
||||
## Connecting
|
||||
|
||||
To connect to an Archipelago server, open the in-game console by pressing backslash `\` and use the command `multiworld connect [address:port] [name] [password]`. The port and password are both optional - if no port is provided then the default port of 38281 is used.
|
||||
To connect to an Archipelago server, open the in-game console by pressing backslash `\` and use
|
||||
the command `multiworld connect [address:port] [name] [password]`.
|
||||
The port and password are both optional - if no port is provided then the default port of 38281 is used.
|
||||
**Make sure to connect to the server before attempting to start a new save file.**
|
||||
35
worlds/clique/Items.py
Normal file
35
worlds/clique/Items.py
Normal file
@@ -0,0 +1,35 @@
|
||||
from typing import Callable, Dict, NamedTuple, Optional
|
||||
|
||||
from BaseClasses import Item, ItemClassification, MultiWorld
|
||||
|
||||
|
||||
class CliqueItem(Item):
|
||||
game = "Clique"
|
||||
|
||||
|
||||
class CliqueItemData(NamedTuple):
|
||||
code: Optional[int] = None
|
||||
type: ItemClassification = ItemClassification.filler
|
||||
can_create: Callable[[MultiWorld, int], bool] = lambda multiworld, player: True
|
||||
|
||||
|
||||
item_data_table: Dict[str, CliqueItemData] = {
|
||||
"Feeling of Satisfaction": CliqueItemData(
|
||||
code=69696969,
|
||||
type=ItemClassification.progression,
|
||||
),
|
||||
"Button Activation": CliqueItemData(
|
||||
code=69696968,
|
||||
type=ItemClassification.progression,
|
||||
can_create=lambda multiworld, player: bool(getattr(multiworld, "hard_mode")[player]),
|
||||
),
|
||||
"A Cool Filler Item (No Satisfaction Guaranteed)": CliqueItemData(
|
||||
code=69696967,
|
||||
can_create=lambda multiworld, player: False # Only created from `get_filler_item_name`.
|
||||
),
|
||||
"The Urge to Push": CliqueItemData(
|
||||
type=ItemClassification.progression,
|
||||
),
|
||||
}
|
||||
|
||||
item_table = {name: data.code for name, data in item_data_table.items() if data.code is not None}
|
||||
34
worlds/clique/Locations.py
Normal file
34
worlds/clique/Locations.py
Normal file
@@ -0,0 +1,34 @@
|
||||
from typing import Callable, Dict, NamedTuple, Optional
|
||||
|
||||
from BaseClasses import Location, MultiWorld
|
||||
|
||||
|
||||
class CliqueLocation(Location):
|
||||
game = "Clique"
|
||||
|
||||
|
||||
class CliqueLocationData(NamedTuple):
|
||||
region: str
|
||||
address: Optional[int] = None
|
||||
can_create: Callable[[MultiWorld, int], bool] = lambda multiworld, player: True
|
||||
locked_item: Optional[str] = None
|
||||
|
||||
|
||||
location_data_table: Dict[str, CliqueLocationData] = {
|
||||
"The Big Red Button": CliqueLocationData(
|
||||
region="The Button Realm",
|
||||
address=69696969,
|
||||
),
|
||||
"The Item on the Desk": CliqueLocationData(
|
||||
region="The Button Realm",
|
||||
address=69696968,
|
||||
can_create=lambda multiworld, player: bool(getattr(multiworld, "hard_mode")[player]),
|
||||
),
|
||||
"In the Player's Mind": CliqueLocationData(
|
||||
region="The Button Realm",
|
||||
locked_item="The Urge to Push",
|
||||
),
|
||||
}
|
||||
|
||||
location_table = {name: data.address for name, data in location_data_table.items() if data.address is not None}
|
||||
locked_locations = {name: data for name, data in location_data_table.items() if data.locked_item}
|
||||
@@ -1,6 +1,6 @@
|
||||
from typing import Dict
|
||||
|
||||
from Options import Option, Toggle
|
||||
from Options import Choice, Option, Toggle
|
||||
|
||||
|
||||
class HardMode(Toggle):
|
||||
@@ -8,6 +8,27 @@ class HardMode(Toggle):
|
||||
display_name = "Hard Mode"
|
||||
|
||||
|
||||
class ButtonColor(Choice):
|
||||
"""Customize your button! Now available in 12 unique colors."""
|
||||
display_name = "Button Color"
|
||||
option_red = 0
|
||||
option_orange = 1
|
||||
option_yellow = 2
|
||||
option_green = 3
|
||||
option_cyan = 4
|
||||
option_blue = 5
|
||||
option_magenta = 6
|
||||
option_purple = 7
|
||||
option_pink = 8
|
||||
option_brown = 9
|
||||
option_white = 10
|
||||
option_black = 11
|
||||
|
||||
|
||||
clique_options: Dict[str, type(Option)] = {
|
||||
"hard_mode": HardMode
|
||||
"color": ButtonColor,
|
||||
"hard_mode": HardMode,
|
||||
|
||||
# DeathLink is always on. Always.
|
||||
# "death_link": DeathLink,
|
||||
}
|
||||
|
||||
11
worlds/clique/Regions.py
Normal file
11
worlds/clique/Regions.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from typing import Dict, List, NamedTuple
|
||||
|
||||
|
||||
class CliqueRegionData(NamedTuple):
|
||||
connecting_regions: List[str] = []
|
||||
|
||||
|
||||
region_data_table: Dict[str, CliqueRegionData] = {
|
||||
"Menu": CliqueRegionData(["The Button Realm"]),
|
||||
"The Button Realm": CliqueRegionData(),
|
||||
}
|
||||
10
worlds/clique/Rules.py
Normal file
10
worlds/clique/Rules.py
Normal file
@@ -0,0 +1,10 @@
|
||||
from typing import Callable
|
||||
|
||||
from BaseClasses import CollectionState, MultiWorld
|
||||
|
||||
|
||||
def get_button_rule(multiworld: MultiWorld, player: int) -> Callable[[CollectionState], bool]:
|
||||
if getattr(multiworld, "hard_mode")[player]:
|
||||
return lambda state: state.has("Button Activation", player)
|
||||
|
||||
return lambda state: True
|
||||
@@ -1,15 +1,12 @@
|
||||
from BaseClasses import Entrance, Item, ItemClassification, Location, MultiWorld, Region, Tutorial
|
||||
from typing import List
|
||||
|
||||
from BaseClasses import Region, Tutorial
|
||||
from worlds.AutoWorld import WebWorld, World
|
||||
from worlds.generic.Rules import set_rule
|
||||
from .Items import CliqueItem, item_data_table, item_table
|
||||
from .Locations import CliqueLocation, location_data_table, location_table, locked_locations
|
||||
from .Options import clique_options
|
||||
|
||||
|
||||
class CliqueItem(Item):
|
||||
game = "Clique"
|
||||
|
||||
|
||||
class CliqueLocation(Location):
|
||||
game = "Clique"
|
||||
from .Regions import region_data_table
|
||||
from .Rules import get_button_rule
|
||||
|
||||
|
||||
class CliqueWebWorld(WebWorld):
|
||||
@@ -27,71 +24,69 @@ class CliqueWebWorld(WebWorld):
|
||||
|
||||
|
||||
class CliqueWorld(World):
|
||||
"""The greatest game ever designed. Full of exciting gameplay!"""
|
||||
"""The greatest game of all time."""
|
||||
|
||||
game = "Clique"
|
||||
data_version = 2
|
||||
data_version = 3
|
||||
web = CliqueWebWorld()
|
||||
option_definitions = clique_options
|
||||
|
||||
# Yes, I'm like 12 for this.
|
||||
location_name_to_id = {
|
||||
"The Big Red Button": 69696969,
|
||||
"The Item on the Desk": 69696968,
|
||||
}
|
||||
|
||||
item_name_to_id = {
|
||||
"Feeling of Satisfaction": 69696969,
|
||||
"Button Activation": 69696968,
|
||||
}
|
||||
location_name_to_id = location_table
|
||||
item_name_to_id = item_table
|
||||
|
||||
def create_item(self, name: str) -> CliqueItem:
|
||||
return CliqueItem(name, ItemClassification.progression, self.item_name_to_id[name], self.player)
|
||||
return CliqueItem(name, item_data_table[name].type, item_data_table[name].code, self.player)
|
||||
|
||||
def create_items(self) -> None:
|
||||
self.multiworld.itempool.append(self.create_item("Feeling of Satisfaction"))
|
||||
self.multiworld.priority_locations[self.player].value.add("The Big Red Button")
|
||||
item_pool: List[CliqueItem] = []
|
||||
for name, item in item_data_table.items():
|
||||
if item.code and item.can_create(self.multiworld, self.player):
|
||||
item_pool.append(self.create_item(name))
|
||||
|
||||
if self.multiworld.hard_mode[self.player]:
|
||||
self.multiworld.itempool.append(self.create_item("Button Activation"))
|
||||
self.multiworld.itempool += item_pool
|
||||
|
||||
def create_regions(self) -> None:
|
||||
if self.multiworld.hard_mode[self.player]:
|
||||
self.multiworld.regions += [
|
||||
create_region(self.multiworld, self.player, "Menu", None, ["The entrance to the button."]),
|
||||
create_region(self.multiworld, self.player, "The realm of the button.", self.location_name_to_id)
|
||||
]
|
||||
else:
|
||||
self.multiworld.regions += [
|
||||
create_region(self.multiworld, self.player, "Menu", None, ["The entrance to the button."]),
|
||||
create_region(self.multiworld, self.player, "The realm of the button.", {
|
||||
"The Big Red Button": 69696969
|
||||
})]
|
||||
# Create regions.
|
||||
for region_name in region_data_table.keys():
|
||||
region = Region(region_name, self.player, self.multiworld)
|
||||
self.multiworld.regions.append(region)
|
||||
|
||||
self.multiworld.get_entrance("The entrance to the button.", self.player) \
|
||||
.connect(self.multiworld.get_region("The realm of the button.", self.player))
|
||||
# Create locations.
|
||||
for region_name, region_data in region_data_table.items():
|
||||
region = self.multiworld.get_region(region_name, self.player)
|
||||
region.add_locations({
|
||||
location_name: location_data.address for location_name, location_data in location_data_table.items()
|
||||
if location_data.region == region_name and location_data.can_create(self.multiworld, self.player)
|
||||
}, CliqueLocation)
|
||||
region.add_exits(region_data_table[region_name].connecting_regions)
|
||||
|
||||
# Place locked locations.
|
||||
for location_name, location_data in locked_locations.items():
|
||||
# Ignore locations we never created.
|
||||
if not location_data.can_create(self.multiworld, self.player):
|
||||
continue
|
||||
|
||||
locked_item = self.create_item(location_data_table[location_name].locked_item)
|
||||
self.multiworld.get_location(location_name, self.player).place_locked_item(locked_item)
|
||||
|
||||
# Set priority location for the Big Red Button!
|
||||
self.multiworld.priority_locations[self.player].value.add("The Big Red Button")
|
||||
|
||||
def get_filler_item_name(self) -> str:
|
||||
return self.multiworld.random.choice(self.item_name_to_id)
|
||||
return "A Cool Filler Item (No Satisfaction Guaranteed)"
|
||||
|
||||
def set_rules(self) -> None:
|
||||
if self.multiworld.hard_mode[self.player]:
|
||||
set_rule(
|
||||
self.multiworld.get_location("The Big Red Button", self.player),
|
||||
lambda state: state.has("Button Activation", self.player))
|
||||
button_rule = get_button_rule(self.multiworld, self.player)
|
||||
self.multiworld.get_location("The Big Red Button", self.player).access_rule = button_rule
|
||||
self.multiworld.get_location("In the Player's Mind", self.player).access_rule = button_rule
|
||||
|
||||
self.multiworld.completion_condition[self.player] = lambda state: \
|
||||
state.has("Feeling of Satisfaction", self.player)
|
||||
# Do not allow button activations on buttons.
|
||||
self.multiworld.get_location("The Big Red Button", self.player).item_rule =\
|
||||
lambda item: item.name != "Button Activation"
|
||||
|
||||
# Completion condition.
|
||||
self.multiworld.completion_condition[self.player] = lambda state: state.has("The Urge to Push", self.player)
|
||||
|
||||
def create_region(world: MultiWorld, player: int, name: str, locations=None, exits=None):
|
||||
region = Region(name, player, world)
|
||||
if locations:
|
||||
for location_name in locations.keys():
|
||||
region.locations.append(CliqueLocation(player, location_name, locations[location_name], region))
|
||||
|
||||
if exits:
|
||||
for _exit in exits:
|
||||
region.exits.append(Entrance(player, _exit, region))
|
||||
|
||||
return region
|
||||
def fill_slot_data(self):
|
||||
return {
|
||||
"color": getattr(self.multiworld, "color")[self.player].current_key
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ Clique is a joke game developed for Archipelago in March 2023 to showcase how ea
|
||||
Archipelago. The objective of the game is to press the big red button. If a player is playing on `hard_mode`, they must
|
||||
wait for someone else in the multiworld to "activate" their button before they can press it.
|
||||
|
||||
Clique can be played on any HTML5-capable browser.
|
||||
Clique can be played on most modern HTML5-capable browsers.
|
||||
|
||||
## Where is the settings page?
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ slot name, and a room password if one is required. Then click "Connect".
|
||||
If you're playing on "easy mode", just click the button and receive "Satisfaction".
|
||||
|
||||
If you're playing on "hard mode", you may need to wait for activation before you can complete your objective. Luckily,
|
||||
Clique runs in all the major browsers that support HTML5, so you can load Clique on your phone and be productive while
|
||||
Clique runs in most major browsers that support HTML5, so you can load Clique on your phone and be productive while
|
||||
you wait!
|
||||
|
||||
If you need some ideas for what to do while waiting for button activation, give the following a try:
|
||||
@@ -19,4 +19,4 @@ If you need some ideas for what to do while waiting for button activation, give
|
||||
- Do your school work.
|
||||
|
||||
|
||||
~~If you run into any issues with this game, definitely do not contact Phar#4444 on discord. *wink* *wink*~~
|
||||
~~If you run into any issues with this game, definitely do not contact **thephar** on discord. *wink* *wink*~~
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,22 +1,691 @@
|
||||
import sys
|
||||
from enum import IntEnum
|
||||
from typing import Optional, NamedTuple, Dict
|
||||
|
||||
from BaseClasses import Location
|
||||
from worlds.dark_souls_3.data.locations_data import location_tables, painted_world_table, dreg_heap_table, \
|
||||
ringed_city_table
|
||||
from BaseClasses import Location, Region
|
||||
|
||||
|
||||
class DS3LocationCategory(IntEnum):
|
||||
WEAPON = 0
|
||||
SHIELD = 1
|
||||
ARMOR = 2
|
||||
RING = 3
|
||||
SPELL = 4
|
||||
NPC = 5
|
||||
KEY = 6
|
||||
BOSS = 7
|
||||
MISC = 8
|
||||
HEALTH = 9
|
||||
PROGRESSIVE_ITEM = 10
|
||||
EVENT = 11
|
||||
|
||||
|
||||
class DS3LocationData(NamedTuple):
|
||||
name: str
|
||||
default_item: str
|
||||
category: DS3LocationCategory
|
||||
|
||||
|
||||
class DarkSouls3Location(Location):
|
||||
game: str = "Dark Souls III"
|
||||
category: DS3LocationCategory
|
||||
default_item_name: str
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
player: int,
|
||||
name: str,
|
||||
category: DS3LocationCategory,
|
||||
default_item_name: str,
|
||||
address: Optional[int] = None,
|
||||
parent: Optional[Region] = None):
|
||||
super().__init__(player, name, address, parent)
|
||||
self.default_item_name = default_item_name
|
||||
self.category = category
|
||||
|
||||
@staticmethod
|
||||
def get_name_to_id() -> dict:
|
||||
base_id = 100000
|
||||
table_offset = 100
|
||||
|
||||
table_order = [
|
||||
"Firelink Shrine",
|
||||
"Firelink Shrine Bell Tower",
|
||||
"High Wall of Lothric",
|
||||
"Undead Settlement",
|
||||
"Road of Sacrifices",
|
||||
"Cathedral of the Deep",
|
||||
"Farron Keep",
|
||||
"Catacombs of Carthus",
|
||||
"Smouldering Lake",
|
||||
"Irithyll of the Boreal Valley",
|
||||
"Irithyll Dungeon",
|
||||
"Profaned Capital",
|
||||
"Anor Londo",
|
||||
"Lothric Castle",
|
||||
"Consumed King's Garden",
|
||||
"Grand Archives",
|
||||
"Untended Graves",
|
||||
"Archdragon Peak",
|
||||
|
||||
"Painted World of Ariandel 1",
|
||||
"Painted World of Ariandel 2",
|
||||
"Dreg Heap",
|
||||
"Ringed City",
|
||||
|
||||
"Progressive Items 1",
|
||||
"Progressive Items 2",
|
||||
"Progressive Items 3",
|
||||
"Progressive Items 4",
|
||||
"Progressive Items DLC",
|
||||
]
|
||||
|
||||
output = {}
|
||||
for i, table in enumerate(location_tables):
|
||||
if len(table) > table_offset:
|
||||
raise Exception("A location table has {} entries, that is more than {} entries (table #{})".format(len(table), table_offset, i))
|
||||
output.update({name: id for id, name in enumerate(table, base_id + (table_offset * i))})
|
||||
for i, region_name in enumerate(table_order):
|
||||
if len(location_tables[region_name]) > table_offset:
|
||||
raise Exception("A location table has {} entries, that is more than {} entries (table #{})".format(len(location_tables[region_name]), table_offset, i))
|
||||
|
||||
output.update({location_data.name: id for id, location_data in enumerate(location_tables[region_name], base_id + (table_offset * i))})
|
||||
|
||||
return output
|
||||
|
||||
|
||||
location_tables = {
|
||||
"Firelink Shrine": [
|
||||
DS3LocationData("FS: Broken Straight Sword", "Broken Straight Sword", DS3LocationCategory.WEAPON),
|
||||
DS3LocationData("FS: East-West Shield", "East-West Shield", DS3LocationCategory.SHIELD),
|
||||
DS3LocationData("FS: Uchigatana", "Uchigatana", DS3LocationCategory.WEAPON),
|
||||
DS3LocationData("FS: Master's Attire", "Master's Attire", DS3LocationCategory.ARMOR),
|
||||
DS3LocationData("FS: Master's Gloves", "Master's Gloves", DS3LocationCategory.ARMOR),
|
||||
],
|
||||
"Firelink Shrine Bell Tower": [
|
||||
DS3LocationData("FSBT: Covetous Silver Serpent Ring", "Covetous Silver Serpent Ring", DS3LocationCategory.RING),
|
||||
DS3LocationData("FSBT: Fire Keeper Robe", "Fire Keeper Robe", DS3LocationCategory.ARMOR),
|
||||
DS3LocationData("FSBT: Fire Keeper Gloves", "Fire Keeper Gloves", DS3LocationCategory.ARMOR),
|
||||
DS3LocationData("FSBT: Fire Keeper Skirt", "Fire Keeper Skirt", DS3LocationCategory.ARMOR),
|
||||
DS3LocationData("FSBT: Estus Ring", "Estus Ring", DS3LocationCategory.RING),
|
||||
DS3LocationData("FSBT: Fire Keeper Soul", "Fire Keeper Soul", DS3LocationCategory.MISC),
|
||||
],
|
||||
"High Wall of Lothric": [
|
||||
DS3LocationData("HWL: Deep Battle Axe", "Deep Battle Axe", DS3LocationCategory.WEAPON),
|
||||
DS3LocationData("HWL: Club", "Club", DS3LocationCategory.WEAPON),
|
||||
DS3LocationData("HWL: Claymore", "Claymore", DS3LocationCategory.WEAPON),
|
||||
DS3LocationData("HWL: Binoculars", "Binoculars", DS3LocationCategory.MISC),
|
||||
DS3LocationData("HWL: Longbow", "Longbow", DS3LocationCategory.WEAPON),
|
||||
DS3LocationData("HWL: Mail Breaker", "Mail Breaker", DS3LocationCategory.WEAPON),
|
||||
DS3LocationData("HWL: Broadsword", "Broadsword", DS3LocationCategory.WEAPON),
|
||||
DS3LocationData("HWL: Silver Eagle Kite Shield", "Silver Eagle Kite Shield", DS3LocationCategory.SHIELD),
|
||||
DS3LocationData("HWL: Astora's Straight Sword", "Astora's Straight Sword", DS3LocationCategory.WEAPON),
|
||||
DS3LocationData("HWL: Cell Key", "Cell Key", DS3LocationCategory.KEY),
|
||||
DS3LocationData("HWL: Rapier", "Rapier", DS3LocationCategory.WEAPON),
|
||||
DS3LocationData("HWL: Lucerne", "Lucerne", DS3LocationCategory.WEAPON),
|
||||
DS3LocationData("HWL: Small Lothric Banner", "Small Lothric Banner", DS3LocationCategory.KEY),
|
||||
DS3LocationData("HWL: Basin of Vows", "Basin of Vows", DS3LocationCategory.KEY),
|
||||
DS3LocationData("HWL: Soul of Boreal Valley Vordt", "Soul of Boreal Valley Vordt", DS3LocationCategory.BOSS),
|
||||
DS3LocationData("HWL: Soul of the Dancer", "Soul of the Dancer", DS3LocationCategory.BOSS),
|
||||
DS3LocationData("HWL: Way of Blue", "Way of Blue", DS3LocationCategory.MISC),
|
||||
DS3LocationData("HWL: Greirat's Ashes", "Greirat's Ashes", DS3LocationCategory.NPC),
|
||||
DS3LocationData("HWL: Blue Tearstone Ring", "Blue Tearstone Ring", DS3LocationCategory.NPC),
|
||||
],
|
||||
"Undead Settlement": [
|
||||
DS3LocationData("US: Small Leather Shield", "Small Leather Shield", DS3LocationCategory.SHIELD),
|
||||
DS3LocationData("US: Whip", "Whip", DS3LocationCategory.WEAPON),
|
||||
DS3LocationData("US: Reinforced Club", "Reinforced Club", DS3LocationCategory.WEAPON),
|
||||
DS3LocationData("US: Blue Wooden Shield", "Blue Wooden Shield", DS3LocationCategory.SHIELD),
|
||||
DS3LocationData("US: Cleric Hat", "Cleric Hat", DS3LocationCategory.ARMOR),
|
||||
DS3LocationData("US: Cleric Blue Robe", "Cleric Blue Robe", DS3LocationCategory.ARMOR),
|
||||
DS3LocationData("US: Cleric Gloves", "Cleric Gloves", DS3LocationCategory.ARMOR),
|
||||
DS3LocationData("US: Cleric Trousers", "Cleric Trousers", DS3LocationCategory.ARMOR),
|
||||
DS3LocationData("US: Mortician's Ashes", "Mortician's Ashes", DS3LocationCategory.KEY),
|
||||
DS3LocationData("US: Caestus", "Caestus", DS3LocationCategory.WEAPON),
|
||||
DS3LocationData("US: Plank Shield", "Plank Shield", DS3LocationCategory.SHIELD),
|
||||
DS3LocationData("US: Flame Stoneplate Ring", "Flame Stoneplate Ring", DS3LocationCategory.RING),
|
||||
DS3LocationData("US: Caduceus Round Shield", "Caduceus Round Shield", DS3LocationCategory.SHIELD),
|
||||
DS3LocationData("US: Fire Clutch Ring", "Fire Clutch Ring", DS3LocationCategory.RING),
|
||||
DS3LocationData("US: Partizan", "Partizan", DS3LocationCategory.WEAPON),
|
||||
DS3LocationData("US: Bloodbite Ring", "Bloodbite Ring", DS3LocationCategory.RING),
|
||||
DS3LocationData("US: Red Hilted Halberd", "Red Hilted Halberd", DS3LocationCategory.WEAPON),
|
||||
DS3LocationData("US: Saint's Talisman", "Saint's Talisman", DS3LocationCategory.WEAPON),
|
||||
DS3LocationData("US: Irithyll Straight Sword", "Irithyll Straight Sword", DS3LocationCategory.WEAPON),
|
||||
DS3LocationData("US: Large Club", "Large Club", DS3LocationCategory.WEAPON),
|
||||
DS3LocationData("US: Northern Helm", "Northern Helm", DS3LocationCategory.ARMOR),
|
||||
DS3LocationData("US: Northern Armor", "Northern Armor", DS3LocationCategory.ARMOR),
|
||||
DS3LocationData("US: Northern Gloves", "Northern Gloves", DS3LocationCategory.ARMOR),
|
||||
DS3LocationData("US: Northern Trousers", "Northern Trousers", DS3LocationCategory.ARMOR),
|
||||
DS3LocationData("US: Flynn's Ring", "Flynn's Ring", DS3LocationCategory.RING),
|
||||
DS3LocationData("US: Mirrah Vest", "Mirrah Vest", DS3LocationCategory.ARMOR),
|
||||
DS3LocationData("US: Mirrah Gloves", "Mirrah Gloves", DS3LocationCategory.ARMOR),
|
||||
DS3LocationData("US: Mirrah Trousers", "Mirrah Trousers", DS3LocationCategory.ARMOR),
|
||||
DS3LocationData("US: Chloranthy Ring", "Chloranthy Ring", DS3LocationCategory.RING),
|
||||
DS3LocationData("US: Loincloth", "Loincloth", DS3LocationCategory.ARMOR),
|
||||
DS3LocationData("US: Wargod Wooden Shield", "Wargod Wooden Shield", DS3LocationCategory.SHIELD),
|
||||
DS3LocationData("US: Loretta's Bone", "Loretta's Bone", DS3LocationCategory.KEY),
|
||||
DS3LocationData("US: Hand Axe", "Hand Axe", DS3LocationCategory.WEAPON),
|
||||
DS3LocationData("US: Great Scythe", "Great Scythe", DS3LocationCategory.WEAPON),
|
||||
DS3LocationData("US: Soul of the Rotted Greatwood", "Soul of the Rotted Greatwood", DS3LocationCategory.BOSS),
|
||||
DS3LocationData("US: Hawk Ring", "Hawk Ring", DS3LocationCategory.RING),
|
||||
DS3LocationData("US: Warrior of Sunlight", "Warrior of Sunlight", DS3LocationCategory.MISC),
|
||||
DS3LocationData("US: Blessed Red and White Shield+1", "Blessed Red and White Shield+1", DS3LocationCategory.SHIELD),
|
||||
DS3LocationData("US: Irina's Ashes", "Irina's Ashes", DS3LocationCategory.NPC),
|
||||
DS3LocationData("US: Cornyx's Ashes", "Cornyx's Ashes", DS3LocationCategory.NPC),
|
||||
DS3LocationData("US: Cornyx's Wrap", "Cornyx's Wrap", DS3LocationCategory.NPC),
|
||||
DS3LocationData("US: Cornyx's Garb", "Cornyx's Garb", DS3LocationCategory.NPC),
|
||||
DS3LocationData("US: Cornyx's Skirt", "Cornyx's Skirt", DS3LocationCategory.NPC),
|
||||
DS3LocationData("US: Pyromancy Flame", "Pyromancy Flame", DS3LocationCategory.NPC),
|
||||
DS3LocationData("US: Transposing Kiln", "Transposing Kiln", DS3LocationCategory.MISC),
|
||||
DS3LocationData("US: Tower Key", "Tower Key", DS3LocationCategory.NPC),
|
||||
],
|
||||
"Road of Sacrifices": [
|
||||
DS3LocationData("RS: Brigand Twindaggers", "Brigand Twindaggers", DS3LocationCategory.WEAPON),
|
||||
DS3LocationData("RS: Brigand Hood", "Brigand Hood", DS3LocationCategory.ARMOR),
|
||||
DS3LocationData("RS: Brigand Armor", "Brigand Armor", DS3LocationCategory.ARMOR),
|
||||
DS3LocationData("RS: Brigand Gauntlets", "Brigand Gauntlets", DS3LocationCategory.ARMOR),
|
||||
DS3LocationData("RS: Brigand Trousers", "Brigand Trousers", DS3LocationCategory.ARMOR),
|
||||
DS3LocationData("RS: Butcher Knife", "Butcher Knife", DS3LocationCategory.WEAPON),
|
||||
DS3LocationData("RS: Brigand Axe", "Brigand Axe", DS3LocationCategory.WEAPON),
|
||||
DS3LocationData("RS: Braille Divine Tome of Carim", "Braille Divine Tome of Carim", DS3LocationCategory.MISC),
|
||||
DS3LocationData("RS: Morne's Ring", "Morne's Ring", DS3LocationCategory.RING),
|
||||
DS3LocationData("RS: Twin Dragon Greatshield", "Twin Dragon Greatshield", DS3LocationCategory.SHIELD),
|
||||
DS3LocationData("RS: Heretic's Staff", "Heretic's Staff", DS3LocationCategory.WEAPON),
|
||||
DS3LocationData("RS: Sorcerer Hood", "Sorcerer Hood", DS3LocationCategory.ARMOR),
|
||||
DS3LocationData("RS: Sorcerer Robe", "Sorcerer Robe", DS3LocationCategory.ARMOR),
|
||||
DS3LocationData("RS: Sorcerer Gloves", "Sorcerer Gloves", DS3LocationCategory.ARMOR),
|
||||
DS3LocationData("RS: Sorcerer Trousers", "Sorcerer Trousers", DS3LocationCategory.ARMOR),
|
||||
DS3LocationData("RS: Sage Ring", "Sage Ring", DS3LocationCategory.RING),
|
||||
DS3LocationData("RS: Fallen Knight Helm", "Fallen Knight Helm", DS3LocationCategory.ARMOR),
|
||||
DS3LocationData("RS: Fallen Knight Armor", "Fallen Knight Armor", DS3LocationCategory.ARMOR),
|
||||
DS3LocationData("RS: Fallen Knight Gauntlets", "Fallen Knight Gauntlets", DS3LocationCategory.ARMOR),
|
||||
DS3LocationData("RS: Fallen Knight Trousers", "Fallen Knight Trousers", DS3LocationCategory.ARMOR),
|
||||
DS3LocationData("RS: Conjurator Hood", "Conjurator Hood", DS3LocationCategory.ARMOR),
|
||||
DS3LocationData("RS: Conjurator Robe", "Conjurator Robe", DS3LocationCategory.ARMOR),
|
||||
DS3LocationData("RS: Conjurator Manchettes", "Conjurator Manchettes", DS3LocationCategory.ARMOR),
|
||||
DS3LocationData("RS: Conjurator Boots", "Conjurator Boots", DS3LocationCategory.ARMOR),
|
||||
DS3LocationData("RS: Great Swamp Pyromancy Tome", "Great Swamp Pyromancy Tome", DS3LocationCategory.MISC),
|
||||
DS3LocationData("RS: Great Club", "Great Club", DS3LocationCategory.WEAPON),
|
||||
DS3LocationData("RS: Exile Greatsword", "Exile Greatsword", DS3LocationCategory.WEAPON),
|
||||
DS3LocationData("RS: Farron Coal", "Farron Coal", DS3LocationCategory.MISC),
|
||||
DS3LocationData("RS: Sellsword Twinblades", "Sellsword Twinblades", DS3LocationCategory.WEAPON),
|
||||
DS3LocationData("RS: Sellsword Helm", "Sellsword Helm", DS3LocationCategory.ARMOR),
|
||||
DS3LocationData("RS: Sellsword Armor", "Sellsword Armor", DS3LocationCategory.ARMOR),
|
||||
DS3LocationData("RS: Sellsword Gauntlet", "Sellsword Gauntlet", DS3LocationCategory.ARMOR),
|
||||
DS3LocationData("RS: Sellsword Trousers", "Sellsword Trousers", DS3LocationCategory.ARMOR),
|
||||
DS3LocationData("RS: Golden Falcon Shield", "Golden Falcon Shield", DS3LocationCategory.SHIELD),
|
||||
DS3LocationData("RS: Herald Helm", "Herald Helm", DS3LocationCategory.ARMOR),
|
||||
DS3LocationData("RS: Herald Armor", "Herald Armor", DS3LocationCategory.ARMOR),
|
||||
DS3LocationData("RS: Herald Gloves", "Herald Gloves", DS3LocationCategory.ARMOR),
|
||||
DS3LocationData("RS: Herald Trousers", "Herald Trousers", DS3LocationCategory.ARMOR),
|
||||
DS3LocationData("RS: Grass Crest Shield", "Grass Crest Shield", DS3LocationCategory.SHIELD),
|
||||
DS3LocationData("RS: Soul of a Crystal Sage", "Soul of a Crystal Sage", DS3LocationCategory.BOSS),
|
||||
DS3LocationData("RS: Great Swamp Ring", "Great Swamp Ring", DS3LocationCategory.RING),
|
||||
DS3LocationData("RS: Orbeck's Ashes", "Orbeck's Ashes", DS3LocationCategory.NPC),
|
||||
],
|
||||
"Cathedral of the Deep": [
|
||||
DS3LocationData("CD: Paladin's Ashes", "Paladin's Ashes", DS3LocationCategory.MISC),
|
||||
DS3LocationData("CD: Spider Shield", "Spider Shield", DS3LocationCategory.SHIELD),
|
||||
DS3LocationData("CD: Crest Shield", "Crest Shield", DS3LocationCategory.SHIELD),
|
||||
DS3LocationData("CD: Notched Whip", "Notched Whip", DS3LocationCategory.WEAPON),
|
||||
DS3LocationData("CD: Astora Greatsword", "Astora Greatsword", DS3LocationCategory.WEAPON),
|
||||
DS3LocationData("CD: Executioner's Greatsword", "Executioner's Greatsword", DS3LocationCategory.WEAPON),
|
||||
DS3LocationData("CD: Curse Ward Greatshield", "Curse Ward Greatshield", DS3LocationCategory.SHIELD),
|
||||
DS3LocationData("CD: Saint-tree Bellvine", "Saint-tree Bellvine", DS3LocationCategory.WEAPON),
|
||||
DS3LocationData("CD: Poisonbite Ring", "Poisonbite Ring", DS3LocationCategory.RING),
|
||||
DS3LocationData("CD: Lloyd's Sword Ring", "Lloyd's Sword Ring", DS3LocationCategory.RING),
|
||||
DS3LocationData("CD: Seek Guidance", "Seek Guidance", DS3LocationCategory.SPELL),
|
||||
DS3LocationData("CD: Aldrich's Sapphire", "Aldrich's Sapphire", DS3LocationCategory.RING),
|
||||
DS3LocationData("CD: Deep Braille Divine Tome", "Deep Braille Divine Tome", DS3LocationCategory.MISC),
|
||||
DS3LocationData("CD: Saint Bident", "Saint Bident", DS3LocationCategory.WEAPON),
|
||||
DS3LocationData("CD: Maiden Hood", "Maiden Hood", DS3LocationCategory.ARMOR),
|
||||
DS3LocationData("CD: Maiden Robe", "Maiden Robe", DS3LocationCategory.ARMOR),
|
||||
DS3LocationData("CD: Maiden Gloves", "Maiden Gloves", DS3LocationCategory.ARMOR),
|
||||
DS3LocationData("CD: Maiden Skirt", "Maiden Skirt", DS3LocationCategory.ARMOR),
|
||||
DS3LocationData("CD: Drang Armor", "Drang Armor", DS3LocationCategory.ARMOR),
|
||||
DS3LocationData("CD: Drang Gauntlets", "Drang Gauntlets", DS3LocationCategory.ARMOR),
|
||||
DS3LocationData("CD: Drang Shoes", "Drang Shoes", DS3LocationCategory.ARMOR),
|
||||
DS3LocationData("CD: Drang Hammers", "Drang Hammers", DS3LocationCategory.WEAPON),
|
||||
DS3LocationData("CD: Deep Ring", "Deep Ring", DS3LocationCategory.RING),
|
||||
DS3LocationData("CD: Archdeacon White Crown", "Archdeacon White Crown", DS3LocationCategory.ARMOR),
|
||||
DS3LocationData("CD: Archdeacon Holy Garb", "Archdeacon Holy Garb", DS3LocationCategory.ARMOR),
|
||||
DS3LocationData("CD: Archdeacon Skirt", "Archdeacon Skirt", DS3LocationCategory.ARMOR),
|
||||
DS3LocationData("CD: Arbalest", "Arbalest", DS3LocationCategory.WEAPON),
|
||||
DS3LocationData("CD: Small Doll", "Small Doll", DS3LocationCategory.KEY),
|
||||
DS3LocationData("CD: Soul of the Deacons of the Deep", "Soul of the Deacons of the Deep", DS3LocationCategory.BOSS),
|
||||
DS3LocationData("CD: Rosaria's Fingers", "Rosaria's Fingers", DS3LocationCategory.MISC)
|
||||
],
|
||||
"Farron Keep": [
|
||||
DS3LocationData("FK: Ragged Mask", "Ragged Mask", DS3LocationCategory.ARMOR),
|
||||
DS3LocationData("FK: Iron Flesh", "Iron Flesh", DS3LocationCategory.SPELL),
|
||||
DS3LocationData("FK: Golden Scroll", "Golden Scroll", DS3LocationCategory.MISC),
|
||||
DS3LocationData("FK: Antiquated Dress", "Antiquated Dress", DS3LocationCategory.ARMOR),
|
||||
DS3LocationData("FK: Antiquated Gloves", "Antiquated Gloves", DS3LocationCategory.ARMOR),
|
||||
DS3LocationData("FK: Antiquated Skirt", "Antiquated Skirt", DS3LocationCategory.ARMOR),
|
||||
DS3LocationData("FK: Nameless Knight Helm", "Nameless Knight Helm", DS3LocationCategory.ARMOR),
|
||||
DS3LocationData("FK: Nameless Knight Armor", "Nameless Knight Armor", DS3LocationCategory.ARMOR),
|
||||
DS3LocationData("FK: Nameless Knight Gauntlets", "Nameless Knight Gauntlets", DS3LocationCategory.ARMOR),
|
||||
DS3LocationData("FK: Nameless Knight Leggings", "Nameless Knight Leggings", DS3LocationCategory.ARMOR),
|
||||
DS3LocationData("FK: Sunlight Talisman", "Sunlight Talisman", DS3LocationCategory.WEAPON),
|
||||
DS3LocationData("FK: Wolf's Blood Swordgrass", "Wolf's Blood Swordgrass", DS3LocationCategory.MISC),
|
||||
DS3LocationData("FK: Greatsword", "Greatsword", DS3LocationCategory.WEAPON),
|
||||
DS3LocationData("FK: Sage's Coal", "Sage's Coal", DS3LocationCategory.MISC),
|
||||
DS3LocationData("FK: Stone Parma", "Stone Parma", DS3LocationCategory.SHIELD),
|
||||
DS3LocationData("FK: Sage's Scroll", "Sage's Scroll", DS3LocationCategory.MISC),
|
||||
DS3LocationData("FK: Crown of Dusk", "Crown of Dusk", DS3LocationCategory.ARMOR),
|
||||
DS3LocationData("FK: Lingering Dragoncrest Ring", "Lingering Dragoncrest Ring", DS3LocationCategory.RING),
|
||||
DS3LocationData("FK: Pharis's Hat", "Pharis's Hat", DS3LocationCategory.ARMOR),
|
||||
DS3LocationData("FK: Black Bow of Pharis", "Black Bow of Pharis", DS3LocationCategory.WEAPON),
|
||||
DS3LocationData("FK: Dreamchaser's Ashes", "Dreamchaser's Ashes", DS3LocationCategory.MISC),
|
||||
DS3LocationData("FK: Great Axe", "Great Axe", DS3LocationCategory.WEAPON),
|
||||
DS3LocationData("FK: Dragon Crest Shield", "Dragon Crest Shield", DS3LocationCategory.SHIELD),
|
||||
DS3LocationData("FK: Lightning Spear", "Lightning Spear", DS3LocationCategory.SPELL),
|
||||
DS3LocationData("FK: Atonement", "Atonement", DS3LocationCategory.SPELL),
|
||||
DS3LocationData("FK: Great Magic Weapon", "Great Magic Weapon", DS3LocationCategory.SPELL),
|
||||
DS3LocationData("FK: Cinders of a Lord - Abyss Watcher", "Cinders of a Lord - Abyss Watcher", DS3LocationCategory.KEY),
|
||||
DS3LocationData("FK: Soul of the Blood of the Wolf", "Soul of the Blood of the Wolf", DS3LocationCategory.BOSS),
|
||||
DS3LocationData("FK: Soul of a Stray Demon", "Soul of a Stray Demon", DS3LocationCategory.BOSS),
|
||||
DS3LocationData("FK: Watchdogs of Farron", "Watchdogs of Farron", DS3LocationCategory.MISC),
|
||||
],
|
||||
"Catacombs of Carthus": [
|
||||
DS3LocationData("CC: Carthus Pyromancy Tome", "Carthus Pyromancy Tome", DS3LocationCategory.MISC),
|
||||
DS3LocationData("CC: Carthus Milkring", "Carthus Milkring", DS3LocationCategory.RING),
|
||||
DS3LocationData("CC: Grave Warden's Ashes", "Grave Warden's Ashes", DS3LocationCategory.MISC),
|
||||
DS3LocationData("CC: Carthus Bloodring", "Carthus Bloodring", DS3LocationCategory.RING),
|
||||
DS3LocationData("CC: Grave Warden Pyromancy Tome", "Grave Warden Pyromancy Tome", DS3LocationCategory.MISC),
|
||||
DS3LocationData("CC: Old Sage's Blindfold", "Old Sage's Blindfold", DS3LocationCategory.ARMOR),
|
||||
DS3LocationData("CC: Witch's Ring", "Witch's Ring", DS3LocationCategory.RING),
|
||||
DS3LocationData("CC: Black Blade", "Black Blade", DS3LocationCategory.WEAPON),
|
||||
DS3LocationData("CC: Soul of High Lord Wolnir", "Soul of High Lord Wolnir", DS3LocationCategory.BOSS),
|
||||
DS3LocationData("CC: Soul of a Demon", "Soul of a Demon", DS3LocationCategory.BOSS),
|
||||
],
|
||||
"Smouldering Lake": [
|
||||
DS3LocationData("SL: Shield of Want", "Shield of Want", DS3LocationCategory.SHIELD),
|
||||
DS3LocationData("SL: Speckled Stoneplate Ring", "Speckled Stoneplate Ring", DS3LocationCategory.RING),
|
||||
DS3LocationData("SL: Dragonrider Bow", "Dragonrider Bow", DS3LocationCategory.WEAPON),
|
||||
DS3LocationData("SL: Lightning Stake", "Lightning Stake", DS3LocationCategory.SPELL),
|
||||
DS3LocationData("SL: Izalith Pyromancy Tome", "Izalith Pyromancy Tome", DS3LocationCategory.MISC),
|
||||
DS3LocationData("SL: Black Knight Sword", "Black Knight Sword", DS3LocationCategory.WEAPON),
|
||||
DS3LocationData("SL: Quelana Pyromancy Tome", "Quelana Pyromancy Tome", DS3LocationCategory.MISC),
|
||||
DS3LocationData("SL: Toxic Mist", "Toxic Mist", DS3LocationCategory.SPELL),
|
||||
DS3LocationData("SL: White Hair Talisman", "White Hair Talisman", DS3LocationCategory.WEAPON),
|
||||
DS3LocationData("SL: Izalith Staff", "Izalith Staff", DS3LocationCategory.WEAPON),
|
||||
DS3LocationData("SL: Sacred Flame", "Sacred Flame", DS3LocationCategory.SPELL),
|
||||
DS3LocationData("SL: Fume Ultra Greatsword", "Fume Ultra Greatsword", DS3LocationCategory.WEAPON),
|
||||
DS3LocationData("SL: Black Iron Greatshield", "Black Iron Greatshield", DS3LocationCategory.SHIELD),
|
||||
DS3LocationData("SL: Soul of the Old Demon King", "Soul of the Old Demon King", DS3LocationCategory.BOSS),
|
||||
DS3LocationData("SL: Knight Slayer's Ring", "Knight Slayer's Ring", DS3LocationCategory.RING),
|
||||
],
|
||||
"Irithyll of the Boreal Valley": [
|
||||
DS3LocationData("IBV: Dorhys' Gnawing", "Dorhys' Gnawing", DS3LocationCategory.SPELL),
|
||||
DS3LocationData("IBV: Witchtree Branch", "Witchtree Branch", DS3LocationCategory.WEAPON),
|
||||
DS3LocationData("IBV: Magic Clutch Ring", "Magic Clutch Ring", DS3LocationCategory.RING),
|
||||
DS3LocationData("IBV: Ring of the Sun's First Born", "Ring of the Sun's First Born", DS3LocationCategory.RING),
|
||||
DS3LocationData("IBV: Roster of Knights", "Roster of Knights", DS3LocationCategory.MISC),
|
||||
DS3LocationData("IBV: Pontiff's Right Eye", "Pontiff's Right Eye", DS3LocationCategory.RING),
|
||||
DS3LocationData("IBV: Yorshka's Spear", "Yorshka's Spear", DS3LocationCategory.WEAPON),
|
||||
DS3LocationData("IBV: Great Heal", "Great Heal", DS3LocationCategory.SPELL),
|
||||
DS3LocationData("IBV: Smough's Great Hammer", "Smough's Great Hammer", DS3LocationCategory.WEAPON),
|
||||
DS3LocationData("IBV: Leo Ring", "Leo Ring", DS3LocationCategory.RING),
|
||||
DS3LocationData("IBV: Excrement-covered Ashes", "Excrement-covered Ashes", DS3LocationCategory.MISC),
|
||||
DS3LocationData("IBV: Dark Stoneplate Ring", "Dark Stoneplate Ring", DS3LocationCategory.RING),
|
||||
DS3LocationData("IBV: Easterner's Ashes", "Easterner's Ashes", DS3LocationCategory.MISC),
|
||||
DS3LocationData("IBV: Painting Guardian's Curved Sword", "Painting Guardian's Curved Sword", DS3LocationCategory.WEAPON),
|
||||
DS3LocationData("IBV: Painting Guardian Hood", "Painting Guardian Hood", DS3LocationCategory.ARMOR),
|
||||
DS3LocationData("IBV: Painting Guardian Gown", "Painting Guardian Gown", DS3LocationCategory.ARMOR),
|
||||
DS3LocationData("IBV: Painting Guardian Gloves", "Painting Guardian Gloves", DS3LocationCategory.ARMOR),
|
||||
DS3LocationData("IBV: Painting Guardian Waistcloth", "Painting Guardian Waistcloth", DS3LocationCategory.ARMOR),
|
||||
DS3LocationData("IBV: Dragonslayer Greatbow", "Dragonslayer Greatbow", DS3LocationCategory.WEAPON),
|
||||
DS3LocationData("IBV: Reversal Ring", "Reversal Ring", DS3LocationCategory.RING),
|
||||
DS3LocationData("IBV: Brass Helm", "Brass Helm", DS3LocationCategory.ARMOR),
|
||||
DS3LocationData("IBV: Brass Armor", "Brass Armor", DS3LocationCategory.ARMOR),
|
||||
DS3LocationData("IBV: Brass Gauntlets", "Brass Gauntlets", DS3LocationCategory.ARMOR),
|
||||
DS3LocationData("IBV: Brass Leggings", "Brass Leggings", DS3LocationCategory.ARMOR),
|
||||
DS3LocationData("IBV: Ring of Favor", "Ring of Favor", DS3LocationCategory.RING),
|
||||
DS3LocationData("IBV: Golden Ritual Spear", "Golden Ritual Spear", DS3LocationCategory.WEAPON),
|
||||
DS3LocationData("IBV: Soul of Pontiff Sulyvahn", "Soul of Pontiff Sulyvahn", DS3LocationCategory.BOSS),
|
||||
DS3LocationData("IBV: Aldrich Faithful", "Aldrich Faithful", DS3LocationCategory.MISC),
|
||||
DS3LocationData("IBV: Drang Twinspears", "Drang Twinspears", DS3LocationCategory.WEAPON),
|
||||
],
|
||||
"Irithyll Dungeon": [
|
||||
DS3LocationData("ID: Bellowing Dragoncrest Ring", "Bellowing Dragoncrest Ring", DS3LocationCategory.RING),
|
||||
DS3LocationData("ID: Jailbreaker's Key", "Jailbreaker's Key", DS3LocationCategory.KEY),
|
||||
DS3LocationData("ID: Prisoner Chief's Ashes", "Prisoner Chief's Ashes", DS3LocationCategory.KEY),
|
||||
DS3LocationData("ID: Old Sorcerer Hat", "Old Sorcerer Hat", DS3LocationCategory.ARMOR),
|
||||
DS3LocationData("ID: Old Sorcerer Coat", "Old Sorcerer Coat", DS3LocationCategory.ARMOR),
|
||||
DS3LocationData("ID: Old Sorcerer Gauntlets", "Old Sorcerer Gauntlets", DS3LocationCategory.ARMOR),
|
||||
DS3LocationData("ID: Old Sorcerer Boots", "Old Sorcerer Boots", DS3LocationCategory.ARMOR),
|
||||
DS3LocationData("ID: Great Magic Shield", "Great Magic Shield", DS3LocationCategory.SPELL),
|
||||
DS3LocationData("ID: Dragon Torso Stone", "Dragon Torso Stone", DS3LocationCategory.MISC),
|
||||
DS3LocationData("ID: Lightning Blade", "Lightning Blade", DS3LocationCategory.SPELL),
|
||||
DS3LocationData("ID: Profaned Coal", "Profaned Coal", DS3LocationCategory.MISC),
|
||||
DS3LocationData("ID: Xanthous Ashes", "Xanthous Ashes", DS3LocationCategory.MISC),
|
||||
DS3LocationData("ID: Old Cell Key", "Old Cell Key", DS3LocationCategory.KEY),
|
||||
DS3LocationData("ID: Pickaxe", "Pickaxe", DS3LocationCategory.WEAPON),
|
||||
DS3LocationData("ID: Profaned Flame", "Profaned Flame", DS3LocationCategory.SPELL),
|
||||
DS3LocationData("ID: Covetous Gold Serpent Ring", "Covetous Gold Serpent Ring", DS3LocationCategory.RING),
|
||||
DS3LocationData("ID: Jailer's Key Ring", "Jailer's Key Ring", DS3LocationCategory.KEY),
|
||||
DS3LocationData("ID: Dusk Crown Ring", "Dusk Crown Ring", DS3LocationCategory.RING),
|
||||
DS3LocationData("ID: Dark Clutch Ring", "Dark Clutch Ring", DS3LocationCategory.RING),
|
||||
DS3LocationData("ID: Karla's Ashes", "Karla's Ashes", DS3LocationCategory.NPC),
|
||||
DS3LocationData("ID: Karla's Pointed Hat", "Karla's Pointed Hat", DS3LocationCategory.NPC),
|
||||
DS3LocationData("ID: Karla's Coat", "Karla's Coat", DS3LocationCategory.NPC),
|
||||
DS3LocationData("ID: Karla's Gloves", "Karla's Gloves", DS3LocationCategory.NPC),
|
||||
DS3LocationData("ID: Karla's Trousers", "Karla's Trousers", DS3LocationCategory.NPC),
|
||||
],
|
||||
"Profaned Capital": [
|
||||
DS3LocationData("PC: Cursebite Ring", "Cursebite Ring", DS3LocationCategory.RING),
|
||||
DS3LocationData("PC: Court Sorcerer Hood", "Court Sorcerer Hood", DS3LocationCategory.ARMOR),
|
||||
DS3LocationData("PC: Court Sorcerer Robe", "Court Sorcerer Robe", DS3LocationCategory.ARMOR),
|
||||
DS3LocationData("PC: Court Sorcerer Gloves", "Court Sorcerer Gloves", DS3LocationCategory.ARMOR),
|
||||
DS3LocationData("PC: Court Sorcerer Trousers", "Court Sorcerer Trousers", DS3LocationCategory.ARMOR),
|
||||
DS3LocationData("PC: Wrath of the Gods", "Wrath of the Gods", DS3LocationCategory.SPELL),
|
||||
DS3LocationData("PC: Logan's Scroll", "Logan's Scroll", DS3LocationCategory.MISC),
|
||||
DS3LocationData("PC: Eleonora", "Eleonora", DS3LocationCategory.WEAPON),
|
||||
DS3LocationData("PC: Court Sorcerer's Staff", "Court Sorcerer's Staff", DS3LocationCategory.WEAPON),
|
||||
DS3LocationData("PC: Greatshield of Glory", "Greatshield of Glory", DS3LocationCategory.SHIELD),
|
||||
DS3LocationData("PC: Storm Ruler", "Storm Ruler", DS3LocationCategory.KEY),
|
||||
DS3LocationData("PC: Cinders of a Lord - Yhorm the Giant", "Cinders of a Lord - Yhorm the Giant", DS3LocationCategory.KEY),
|
||||
DS3LocationData("PC: Soul of Yhorm the Giant", "Soul of Yhorm the Giant", DS3LocationCategory.BOSS),
|
||||
],
|
||||
"Anor Londo": [
|
||||
DS3LocationData("AL: Giant's Coal", "Giant's Coal", DS3LocationCategory.MISC),
|
||||
DS3LocationData("AL: Sun Princess Ring", "Sun Princess Ring", DS3LocationCategory.RING),
|
||||
DS3LocationData("AL: Aldrich's Ruby", "Aldrich's Ruby", DS3LocationCategory.RING),
|
||||
DS3LocationData("AL: Cinders of a Lord - Aldrich", "Cinders of a Lord - Aldrich", DS3LocationCategory.KEY),
|
||||
DS3LocationData("AL: Soul of Aldrich", "Soul of Aldrich", DS3LocationCategory.BOSS),
|
||||
],
|
||||
"Lothric Castle": [
|
||||
DS3LocationData("LC: Hood of Prayer", "Hood of Prayer", DS3LocationCategory.ARMOR),
|
||||
DS3LocationData("LC: Robe of Prayer", "Robe of Prayer", DS3LocationCategory.ARMOR),
|
||||
DS3LocationData("LC: Skirt of Prayer", "Skirt of Prayer", DS3LocationCategory.ARMOR),
|
||||
DS3LocationData("LC: Sacred Bloom Shield", "Sacred Bloom Shield", DS3LocationCategory.SHIELD),
|
||||
DS3LocationData("LC: Winged Knight Helm", "Winged Knight Helm", DS3LocationCategory.ARMOR),
|
||||
DS3LocationData("LC: Winged Knight Armor", "Winged Knight Armor", DS3LocationCategory.ARMOR),
|
||||
DS3LocationData("LC: Winged Knight Gauntlets", "Winged Knight Gauntlets", DS3LocationCategory.ARMOR),
|
||||
DS3LocationData("LC: Winged Knight Leggings", "Winged Knight Leggings", DS3LocationCategory.ARMOR),
|
||||
DS3LocationData("LC: Greatlance", "Greatlance", DS3LocationCategory.WEAPON),
|
||||
DS3LocationData("LC: Sniper Crossbow", "Sniper Crossbow", DS3LocationCategory.WEAPON),
|
||||
DS3LocationData("LC: Spirit Tree Crest Shield", "Spirit Tree Crest Shield", DS3LocationCategory.SHIELD),
|
||||
DS3LocationData("LC: Red Tearstone Ring", "Red Tearstone Ring", DS3LocationCategory.RING),
|
||||
DS3LocationData("LC: Caitha's Chime", "Caitha's Chime", DS3LocationCategory.WEAPON),
|
||||
DS3LocationData("LC: Braille Divine Tome of Lothric", "Braille Divine Tome of Lothric", DS3LocationCategory.MISC),
|
||||
DS3LocationData("LC: Knight's Ring", "Knight's Ring", DS3LocationCategory.RING),
|
||||
DS3LocationData("LC: Irithyll Rapier", "Irithyll Rapier", DS3LocationCategory.WEAPON),
|
||||
DS3LocationData("LC: Sunlight Straight Sword", "Sunlight Straight Sword", DS3LocationCategory.WEAPON),
|
||||
DS3LocationData("LC: Soul of Dragonslayer Armour", "Soul of Dragonslayer Armour", DS3LocationCategory.BOSS),
|
||||
DS3LocationData("LC: Grand Archives Key", "Grand Archives Key", DS3LocationCategory.KEY),
|
||||
DS3LocationData("LC: Gotthard Twinswords", "Gotthard Twinswords", DS3LocationCategory.WEAPON),
|
||||
],
|
||||
"Consumed King's Garden": [
|
||||
DS3LocationData("CKG: Dragonscale Ring", "Dragonscale Ring", DS3LocationCategory.RING),
|
||||
DS3LocationData("CKG: Shadow Mask", "Shadow Mask", DS3LocationCategory.ARMOR),
|
||||
DS3LocationData("CKG: Shadow Garb", "Shadow Garb", DS3LocationCategory.ARMOR),
|
||||
DS3LocationData("CKG: Shadow Gauntlets", "Shadow Gauntlets", DS3LocationCategory.ARMOR),
|
||||
DS3LocationData("CKG: Shadow Leggings", "Shadow Leggings", DS3LocationCategory.ARMOR),
|
||||
DS3LocationData("CKG: Claw", "Claw", DS3LocationCategory.WEAPON),
|
||||
DS3LocationData("CKG: Soul of Consumed Oceiros", "Soul of Consumed Oceiros", DS3LocationCategory.BOSS),
|
||||
DS3LocationData("CKG: Magic Stoneplate Ring", "Magic Stoneplate Ring", DS3LocationCategory.RING),
|
||||
],
|
||||
"Grand Archives": [
|
||||
DS3LocationData("GA: Avelyn", "Avelyn", DS3LocationCategory.WEAPON),
|
||||
DS3LocationData("GA: Witch's Locks", "Witch's Locks", DS3LocationCategory.WEAPON),
|
||||
DS3LocationData("GA: Power Within", "Power Within", DS3LocationCategory.SPELL),
|
||||
DS3LocationData("GA: Scholar Ring", "Scholar Ring", DS3LocationCategory.RING),
|
||||
DS3LocationData("GA: Soul Stream", "Soul Stream", DS3LocationCategory.SPELL),
|
||||
DS3LocationData("GA: Fleshbite Ring", "Fleshbite Ring", DS3LocationCategory.RING),
|
||||
DS3LocationData("GA: Crystal Chime", "Crystal Chime", DS3LocationCategory.WEAPON),
|
||||
DS3LocationData("GA: Golden Wing Crest Shield", "Golden Wing Crest Shield", DS3LocationCategory.SHIELD),
|
||||
DS3LocationData("GA: Onikiri and Ubadachi", "Onikiri and Ubadachi", DS3LocationCategory.WEAPON),
|
||||
DS3LocationData("GA: Hunter's Ring", "Hunter's Ring", DS3LocationCategory.RING),
|
||||
DS3LocationData("GA: Divine Pillars of Light", "Divine Pillars of Light", DS3LocationCategory.SPELL),
|
||||
DS3LocationData("GA: Cinders of a Lord - Lothric Prince", "Cinders of a Lord - Lothric Prince", DS3LocationCategory.KEY),
|
||||
DS3LocationData("GA: Soul of the Twin Princes", "Soul of the Twin Princes", DS3LocationCategory.BOSS),
|
||||
DS3LocationData("GA: Sage's Crystal Staff", "Sage's Crystal Staff", DS3LocationCategory.WEAPON),
|
||||
DS3LocationData("GA: Outrider Knight Helm", "Outrider Knight Helm", DS3LocationCategory.ARMOR),
|
||||
DS3LocationData("GA: Outrider Knight Armor", "Outrider Knight Armor", DS3LocationCategory.ARMOR),
|
||||
DS3LocationData("GA: Outrider Knight Gauntlets", "Outrider Knight Gauntlets", DS3LocationCategory.ARMOR),
|
||||
DS3LocationData("GA: Outrider Knight Leggings", "Outrider Knight Leggings", DS3LocationCategory.ARMOR),
|
||||
DS3LocationData("GA: Crystal Scroll", "Crystal Scroll", DS3LocationCategory.MISC),
|
||||
],
|
||||
"Untended Graves": [
|
||||
DS3LocationData("UG: Ashen Estus Ring", "Ashen Estus Ring", DS3LocationCategory.RING),
|
||||
DS3LocationData("UG: Black Knight Glaive", "Black Knight Glaive", DS3LocationCategory.WEAPON),
|
||||
DS3LocationData("UG: Hornet Ring", "Hornet Ring", DS3LocationCategory.RING),
|
||||
DS3LocationData("UG: Chaos Blade", "Chaos Blade", DS3LocationCategory.WEAPON),
|
||||
DS3LocationData("UG: Blacksmith Hammer", "Blacksmith Hammer", DS3LocationCategory.WEAPON),
|
||||
DS3LocationData("UG: Eyes of a Fire Keeper", "Eyes of a Fire Keeper", DS3LocationCategory.KEY),
|
||||
DS3LocationData("UG: Coiled Sword Fragment", "Coiled Sword Fragment", DS3LocationCategory.MISC),
|
||||
DS3LocationData("UG: Soul of Champion Gundyr", "Soul of Champion Gundyr", DS3LocationCategory.BOSS),
|
||||
],
|
||||
"Archdragon Peak": [
|
||||
DS3LocationData("AP: Lightning Clutch Ring", "Lightning Clutch Ring", DS3LocationCategory.RING),
|
||||
DS3LocationData("AP: Ancient Dragon Greatshield", "Ancient Dragon Greatshield", DS3LocationCategory.SHIELD),
|
||||
DS3LocationData("AP: Ring of Steel Protection", "Ring of Steel Protection", DS3LocationCategory.RING),
|
||||
DS3LocationData("AP: Calamity Ring", "Calamity Ring", DS3LocationCategory.RING),
|
||||
DS3LocationData("AP: Drakeblood Greatsword", "Drakeblood Greatsword", DS3LocationCategory.WEAPON),
|
||||
DS3LocationData("AP: Dragonslayer Spear", "Dragonslayer Spear", DS3LocationCategory.WEAPON),
|
||||
DS3LocationData("AP: Thunder Stoneplate Ring", "Thunder Stoneplate Ring", DS3LocationCategory.RING),
|
||||
DS3LocationData("AP: Great Magic Barrier", "Great Magic Barrier", DS3LocationCategory.SPELL),
|
||||
DS3LocationData("AP: Dragon Chaser's Ashes", "Dragon Chaser's Ashes", DS3LocationCategory.MISC),
|
||||
DS3LocationData("AP: Twinkling Dragon Torso Stone", "Twinkling Dragon Torso Stone", DS3LocationCategory.MISC),
|
||||
DS3LocationData("AP: Dragonslayer Helm", "Dragonslayer Helm", DS3LocationCategory.ARMOR),
|
||||
DS3LocationData("AP: Dragonslayer Armor", "Dragonslayer Armor", DS3LocationCategory.ARMOR),
|
||||
DS3LocationData("AP: Dragonslayer Gauntlets", "Dragonslayer Gauntlets", DS3LocationCategory.ARMOR),
|
||||
DS3LocationData("AP: Dragonslayer Leggings", "Dragonslayer Leggings", DS3LocationCategory.ARMOR),
|
||||
DS3LocationData("AP: Ricard's Rapier", "Ricard's Rapier", DS3LocationCategory.WEAPON),
|
||||
DS3LocationData("AP: Soul of the Nameless King", "Soul of the Nameless King", DS3LocationCategory.BOSS),
|
||||
DS3LocationData("AP: Dragon Tooth", "Dragon Tooth", DS3LocationCategory.WEAPON),
|
||||
DS3LocationData("AP: Havel's Greatshield", "Havel's Greatshield", DS3LocationCategory.SHIELD),
|
||||
],
|
||||
"Kiln of the First Flame": [],
|
||||
|
||||
# DLC
|
||||
"Painted World of Ariandel 1": [
|
||||
DS3LocationData("PW: Follower Javelin", "Follower Javelin", DS3LocationCategory.WEAPON),
|
||||
DS3LocationData("PW: Frozen Weapon", "Frozen Weapon", DS3LocationCategory.SPELL),
|
||||
DS3LocationData("PW: Millwood Greatbow", "Millwood Greatbow", DS3LocationCategory.WEAPON),
|
||||
DS3LocationData("PW: Captain's Ashes", "Captain's Ashes", DS3LocationCategory.MISC),
|
||||
DS3LocationData("PW: Millwood Battle Axe", "Millwood Battle Axe", DS3LocationCategory.WEAPON),
|
||||
DS3LocationData("PW: Ethereal Oak Shield", "Ethereal Oak Shield", DS3LocationCategory.SHIELD),
|
||||
DS3LocationData("PW: Crow Quills", "Crow Quills", DS3LocationCategory.WEAPON),
|
||||
DS3LocationData("PW: Slave Knight Hood", "Slave Knight Hood", DS3LocationCategory.ARMOR),
|
||||
DS3LocationData("PW: Slave Knight Armor", "Slave Knight Armor", DS3LocationCategory.ARMOR),
|
||||
DS3LocationData("PW: Slave Knight Gauntlets", "Slave Knight Gauntlets", DS3LocationCategory.ARMOR),
|
||||
DS3LocationData("PW: Slave Knight Leggings", "Slave Knight Leggings", DS3LocationCategory.ARMOR),
|
||||
DS3LocationData("PW: Way of White Corona", "Way of White Corona", DS3LocationCategory.SPELL),
|
||||
DS3LocationData("PW: Crow Talons", "Crow Talons", DS3LocationCategory.WEAPON),
|
||||
DS3LocationData("PW: Onyx Blade", "Onyx Blade", DS3LocationCategory.WEAPON),
|
||||
DS3LocationData("PW: Contraption Key", "Contraption Key", DS3LocationCategory.KEY),
|
||||
],
|
||||
"Painted World of Ariandel 2": [
|
||||
DS3LocationData("PW: Quakestone Hammer", "Quakestone Hammer", DS3LocationCategory.WEAPON),
|
||||
DS3LocationData("PW: Earth Seeker", "Earth Seeker", DS3LocationCategory.WEAPON),
|
||||
DS3LocationData("PW: Follower Torch", "Follower Torch", DS3LocationCategory.SHIELD),
|
||||
DS3LocationData("PW: Follower Shield", "Follower Shield", DS3LocationCategory.SHIELD),
|
||||
DS3LocationData("PW: Follower Sabre", "Follower Sabre", DS3LocationCategory.WEAPON),
|
||||
DS3LocationData("PW: Snap Freeze", "Snap Freeze", DS3LocationCategory.SPELL),
|
||||
DS3LocationData("PW: Floating Chaos", "Floating Chaos", DS3LocationCategory.SPELL),
|
||||
DS3LocationData("PW: Pyromancer's Parting Flame", "Pyromancer's Parting Flame", DS3LocationCategory.WEAPON),
|
||||
DS3LocationData("PW: Vilhelm's Helm", "Vilhelm's Helm", DS3LocationCategory.ARMOR),
|
||||
DS3LocationData("PW: Vilhelm's Armor", "Vilhelm's Armor", DS3LocationCategory.ARMOR),
|
||||
DS3LocationData("PW: Vilhelm's Gauntlets", "Vilhelm's Gauntlets", DS3LocationCategory.ARMOR),
|
||||
DS3LocationData("PW: Vilhelm's Leggings", "Vilhelm's Leggings", DS3LocationCategory.ARMOR),
|
||||
DS3LocationData("PW: Valorheart", "Valorheart", DS3LocationCategory.WEAPON),
|
||||
DS3LocationData("PW: Champion's Bones", "Champion's Bones", DS3LocationCategory.MISC),
|
||||
DS3LocationData("PW: Soul of Sister Friede", "Soul of Sister Friede", DS3LocationCategory.BOSS),
|
||||
DS3LocationData("PW: Chillbite Ring", "Chillbite Ring", DS3LocationCategory.RING),
|
||||
],
|
||||
"Dreg Heap": [
|
||||
DS3LocationData("DH: Loincloth", "Loincloth", DS3LocationCategory.ARMOR),
|
||||
DS3LocationData("DH: Aquamarine Dagger", "Aquamarine Dagger", DS3LocationCategory.WEAPON),
|
||||
DS3LocationData("DH: Murky Hand Scythe", "Murky Hand Scythe", DS3LocationCategory.WEAPON),
|
||||
DS3LocationData("DH: Murky Longstaff", "Murky Longstaff", DS3LocationCategory.WEAPON),
|
||||
DS3LocationData("DH: Great Soul Dregs", "Great Soul Dregs", DS3LocationCategory.SPELL),
|
||||
DS3LocationData("DH: Lothric War Banner", "Lothric War Banner", DS3LocationCategory.WEAPON),
|
||||
DS3LocationData("DH: Projected Heal", "Projected Heal", DS3LocationCategory.SPELL),
|
||||
DS3LocationData("DH: Desert Pyromancer Hood", "Desert Pyromancer Hood", DS3LocationCategory.ARMOR),
|
||||
DS3LocationData("DH: Desert Pyromancer Garb", "Desert Pyromancer Garb", DS3LocationCategory.ARMOR),
|
||||
DS3LocationData("DH: Desert Pyromancer Gloves", "Desert Pyromancer Gloves", DS3LocationCategory.ARMOR),
|
||||
DS3LocationData("DH: Desert Pyromancer Skirt", "Desert Pyromancer Skirt", DS3LocationCategory.ARMOR),
|
||||
DS3LocationData("DH: Giant Door Shield", "Giant Door Shield", DS3LocationCategory.SHIELD),
|
||||
DS3LocationData("DH: Herald Curved Greatsword", "Herald Curved Greatsword", DS3LocationCategory.WEAPON),
|
||||
DS3LocationData("DH: Flame Fan", "Flame Fan", DS3LocationCategory.SPELL),
|
||||
DS3LocationData("DH: Soul of the Demon Prince", "Soul of the Demon Prince", DS3LocationCategory.BOSS),
|
||||
DS3LocationData("DH: Small Envoy Banner", "Small Envoy Banner", DS3LocationCategory.KEY),
|
||||
DS3LocationData("DH: Ring of Favor+3", "Ring of Favor+3", DS3LocationCategory.RING),
|
||||
DS3LocationData("DH: Covetous Silver Serpent Ring+3", "Covetous Silver Serpent Ring+3", DS3LocationCategory.RING),
|
||||
DS3LocationData("DH: Ring of Steel Protection+3", "Ring of Steel Protection+3", DS3LocationCategory.RING),
|
||||
],
|
||||
"Ringed City": [
|
||||
DS3LocationData("RC: Ruin Sentinel Helm", "Ruin Sentinel Helm", DS3LocationCategory.ARMOR),
|
||||
DS3LocationData("RC: Ruin Sentinel Armor", "Ruin Sentinel Armor", DS3LocationCategory.ARMOR),
|
||||
DS3LocationData("RC: Ruin Sentinel Gauntlets", "Ruin Sentinel Gauntlets", DS3LocationCategory.ARMOR),
|
||||
DS3LocationData("RC: Ruin Sentinel Leggings", "Ruin Sentinel Leggings", DS3LocationCategory.ARMOR),
|
||||
DS3LocationData("RC: Black Witch Veil", "Black Witch Veil", DS3LocationCategory.ARMOR),
|
||||
DS3LocationData("RC: Black Witch Hat", "Black Witch Hat", DS3LocationCategory.ARMOR),
|
||||
DS3LocationData("RC: Black Witch Garb", "Black Witch Garb", DS3LocationCategory.ARMOR),
|
||||
DS3LocationData("RC: Black Witch Wrappings", "Black Witch Wrappings", DS3LocationCategory.ARMOR),
|
||||
DS3LocationData("RC: Black Witch Trousers", "Black Witch Trousers", DS3LocationCategory.ARMOR),
|
||||
DS3LocationData("RC: White Preacher Head", "White Preacher Head", DS3LocationCategory.ARMOR),
|
||||
DS3LocationData("RC: Havel's Ring+3", "Havel's Ring+3", DS3LocationCategory.RING),
|
||||
DS3LocationData("RC: Ringed Knight Spear", "Ringed Knight Spear", DS3LocationCategory.WEAPON),
|
||||
DS3LocationData("RC: Dragonhead Shield", "Dragonhead Shield", DS3LocationCategory.SHIELD),
|
||||
DS3LocationData("RC: Ringed Knight Straight Sword", "Ringed Knight Straight Sword", DS3LocationCategory.WEAPON),
|
||||
DS3LocationData("RC: Preacher's Right Arm", "Preacher's Right Arm", DS3LocationCategory.WEAPON),
|
||||
DS3LocationData("RC: White Birch Bow", "White Birch Bow", DS3LocationCategory.WEAPON),
|
||||
DS3LocationData("RC: Church Guardian Shiv", "Church Guardian Shiv", DS3LocationCategory.MISC),
|
||||
DS3LocationData("RC: Dragonhead Greatshield", "Dragonhead Greatshield", DS3LocationCategory.SHIELD),
|
||||
DS3LocationData("RC: Ringed Knight Paired Greatswords", "Ringed Knight Paired Greatswords", DS3LocationCategory.WEAPON),
|
||||
DS3LocationData("RC: Shira's Crown", "Shira's Crown", DS3LocationCategory.ARMOR),
|
||||
DS3LocationData("RC: Shira's Armor", "Shira's Armor", DS3LocationCategory.ARMOR),
|
||||
DS3LocationData("RC: Shira's Gloves", "Shira's Gloves", DS3LocationCategory.ARMOR),
|
||||
DS3LocationData("RC: Shira's Trousers", "Shira's Trousers", DS3LocationCategory.ARMOR),
|
||||
DS3LocationData("RC: Crucifix of the Mad King", "Crucifix of the Mad King", DS3LocationCategory.WEAPON),
|
||||
DS3LocationData("RC: Sacred Chime of Filianore", "Sacred Chime of Filianore", DS3LocationCategory.WEAPON),
|
||||
DS3LocationData("RC: Iron Dragonslayer Helm", "Iron Dragonslayer Helm", DS3LocationCategory.ARMOR),
|
||||
DS3LocationData("RC: Iron Dragonslayer Armor", "Iron Dragonslayer Armor", DS3LocationCategory.ARMOR),
|
||||
DS3LocationData("RC: Iron Dragonslayer Gauntlets", "Iron Dragonslayer Gauntlets", DS3LocationCategory.ARMOR),
|
||||
DS3LocationData("RC: Iron Dragonslayer Leggings", "Iron Dragonslayer Leggings", DS3LocationCategory.ARMOR),
|
||||
DS3LocationData("RC: Lightning Arrow", "Lightning Arrow", DS3LocationCategory.SPELL),
|
||||
DS3LocationData("RC: Ritual Spear Fragment", "Ritual Spear Fragment", DS3LocationCategory.MISC),
|
||||
DS3LocationData("RC: Antiquated Plain Garb", "Antiquated Plain Garb", DS3LocationCategory.ARMOR),
|
||||
DS3LocationData("RC: Violet Wrappings", "Violet Wrappings", DS3LocationCategory.ARMOR),
|
||||
DS3LocationData("RC: Soul of Darkeater Midir", "Soul of Darkeater Midir", DS3LocationCategory.BOSS),
|
||||
DS3LocationData("RC: Soul of Slave Knight Gael", "Soul of Slave Knight Gael", DS3LocationCategory.BOSS),
|
||||
DS3LocationData("RC: Blood of the Dark Soul", "Blood of the Dark Soul", DS3LocationCategory.KEY),
|
||||
DS3LocationData("RC: Chloranthy Ring+3", "Chloranthy Ring+3", DS3LocationCategory.RING),
|
||||
DS3LocationData("RC: Covetous Gold Serpent Ring+3", "Covetous Gold Serpent Ring+3", DS3LocationCategory.RING),
|
||||
DS3LocationData("RC: Ring of the Evil Eye+3", "Ring of the Evil Eye+3", DS3LocationCategory.RING),
|
||||
DS3LocationData("RC: Wolf Ring+3", "Wolf Ring+3", DS3LocationCategory.RING),
|
||||
],
|
||||
|
||||
# Progressive
|
||||
"Progressive Items 1": [] +
|
||||
# Upgrade materials
|
||||
[DS3LocationData(f"Titanite Shard #{i + 1}", "Titanite Shard", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(26)] +
|
||||
[DS3LocationData(f"Large Titanite Shard #{i + 1}", "Large Titanite Shard", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(28)] +
|
||||
[DS3LocationData(f"Titanite Slab #{i + 1}", "Titanite Slab", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(3)] +
|
||||
[DS3LocationData(f"Twinkling Titanite #{i + 1}", "Twinkling Titanite", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(15)] +
|
||||
|
||||
# Healing
|
||||
[DS3LocationData(f"Estus Shard #{i + 1}", "Estus Shard", DS3LocationCategory.HEALTH) for i in range(11)] +
|
||||
[DS3LocationData(f"Undead Bone Shard #{i + 1}", "Undead Bone Shard", DS3LocationCategory.HEALTH) for i in range(10)],
|
||||
|
||||
"Progressive Items 2": [] +
|
||||
# Items
|
||||
[DS3LocationData(f"Green Blossom #{i + 1}", "Green Blossom", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(6)] +
|
||||
[DS3LocationData(f"Firebomb #{i + 1}", "Firebomb", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(4)] +
|
||||
[DS3LocationData(f"Alluring Skull #{i + 1}", "Alluring Skull", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(3)] +
|
||||
[DS3LocationData(f"Undead Hunter Charm #{i + 1}", "Undead Hunter Charm", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(2)] +
|
||||
[DS3LocationData(f"Duel Charm #{i + 1}", "Duel Charm", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(2)] +
|
||||
[DS3LocationData(f"Throwing Knife #{i + 1}", "Throwing Knife", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(2)] +
|
||||
[DS3LocationData(f"Gold Pine Resin #{i + 1}", "Gold Pine Resin", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(1)] +
|
||||
[DS3LocationData(f"Charcoal Pine Resin #{i + 1}", "Charcoal Pine Resin", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(1)] +
|
||||
[DS3LocationData(f"Human Pine Resin #{i + 1}", "Human Pine Resin", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(2)] +
|
||||
[DS3LocationData(f"Carthus Rouge #{i + 1}", "Carthus Rouge", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(1)] +
|
||||
[DS3LocationData(f"Pale Pine Resin #{i + 1}", "Pale Pine Resin", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(1)] +
|
||||
[DS3LocationData(f"Charcoal Pine Bundle #{i + 1}", "Charcoal Pine Bundle", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(2)] +
|
||||
[DS3LocationData(f"Rotten Pine Resin #{i + 1}", "Rotten Pine Resin", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(2)] +
|
||||
[DS3LocationData(f"Homeward Bone #{i + 1}", "Homeward Bone", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(16)] +
|
||||
[DS3LocationData(f"Pale Tongue #{i + 1}", "Pale Tongue", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(3)] +
|
||||
[DS3LocationData(f"Rusted Coin #{i + 1}", "Rusted Coin", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(2)] +
|
||||
[DS3LocationData(f"Rusted Gold Coin #{i + 1}", "Rusted Gold Coin", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(3)] +
|
||||
[DS3LocationData(f"Ember #{i + 1}", "Ember", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(45)],
|
||||
|
||||
"Progressive Items 3": [] +
|
||||
# Souls & Bulk Upgrade Materials
|
||||
[DS3LocationData(f"Fading Soul #{i + 1}", "Fading Soul", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(5)] +
|
||||
[DS3LocationData(f"Soul of a Deserted Corpse #{i + 1}", "Soul of a Deserted Corpse", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(5)] +
|
||||
[DS3LocationData(f"Large Soul of a Deserted Corpse #{i + 1}", "Large Soul of a Deserted Corpse", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(5)] +
|
||||
[DS3LocationData(f"Soul of an Unknown Traveler #{i + 1}", "Soul of an Unknown Traveler", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(5)] +
|
||||
[DS3LocationData(f"Large Soul of an Unknown Traveler #{i + 1}", "Large Soul of an Unknown Traveler", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(5)] +
|
||||
[DS3LocationData(f"Soul of a Nameless Soldier #{i + 1}", "Soul of a Nameless Soldier", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(4)] +
|
||||
[DS3LocationData(f"Large Soul of a Nameless Soldier #{i + 1}", "Large Soul of a Nameless Soldier", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(4)] +
|
||||
[DS3LocationData(f"Soul of a Weary Warrior #{i + 1}", "Soul of a Weary Warrior", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(6)] +
|
||||
[DS3LocationData(f"Soul of a Crestfallen Knight #{i + 1}", "Soul of a Crestfallen Knight", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(2)] +
|
||||
[DS3LocationData(f"Titanite Chunk #{i + 1}", "Titanite Chunk", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(22)] +
|
||||
[DS3LocationData(f"Titanite Scale #{i + 1}", "Titanite Scale", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(29)],
|
||||
|
||||
"Progressive Items 4": [] +
|
||||
# Gems & Random Consumables
|
||||
[DS3LocationData(f"Ring of Sacrifice #{i + 1}", "Ring of Sacrifice", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(4)] +
|
||||
[DS3LocationData(f"Divine Blessing #{i + 1}", "Divine Blessing", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(2)] +
|
||||
[DS3LocationData(f"Hidden Blessing #{i + 1}", "Hidden Blessing", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(1)] +
|
||||
[DS3LocationData(f"Budding Green Blossom #{i + 1}", "Budding Green Blossom", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(5)] +
|
||||
[DS3LocationData(f"Bloodred Moss Clump #{i + 1}", "Bloodred Moss Clump", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(1)] +
|
||||
[DS3LocationData(f"Purple Moss Clump #{i + 1}", "Purple Moss Clump", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(3)] +
|
||||
[DS3LocationData(f"Blooming Purple Moss Clump #{i + 1}", "Blooming Purple Moss Clump", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(1)] +
|
||||
[DS3LocationData(f"Purging Stone #{i + 1}", "Purging Stone", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(2)] +
|
||||
[DS3LocationData(f"Rime-blue Moss Clump #{i + 1}", "Rime-blue Moss Clump", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(2)] +
|
||||
[DS3LocationData(f"Repair Powder #{i + 1}", "Repair Powder", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(3)] +
|
||||
[DS3LocationData(f"Kukri #{i + 1}", "Kukri", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(2)] +
|
||||
[DS3LocationData(f"Lightning Urn #{i + 1}", "Lightning Urn", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(3)] +
|
||||
[DS3LocationData(f"Rubbish #{i + 1}", "Rubbish", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(1)] +
|
||||
[DS3LocationData(f"Blue Bug Pellet #{i + 1}", "Blue Bug Pellet", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(3)] +
|
||||
[DS3LocationData(f"Red Bug Pellet #{i + 1}", "Red Bug Pellet", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(3)] +
|
||||
[DS3LocationData(f"Yellow Bug Pellet #{i + 1}", "Yellow Bug Pellet", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(3)] +
|
||||
[DS3LocationData(f"Black Bug Pellet #{i + 1}", "Black Bug Pellet", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(3)] +
|
||||
[DS3LocationData(f"Heavy Gem #{i + 1}", "Heavy Gem", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(3)] +
|
||||
[DS3LocationData(f"Sharp Gem #{i + 1}", "Sharp Gem", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(3)] +
|
||||
[DS3LocationData(f"Refined Gem #{i + 1}", "Refined Gem", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(3)] +
|
||||
[DS3LocationData(f"Crystal Gem #{i + 1}", "Crystal Gem", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(3)] +
|
||||
[DS3LocationData(f"Simple Gem #{i + 1}", "Simple Gem", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(2)] +
|
||||
[DS3LocationData(f"Fire Gem #{i + 1}", "Fire Gem", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(2)] +
|
||||
[DS3LocationData(f"Chaos Gem #{i + 1}", "Chaos Gem", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(2)] +
|
||||
[DS3LocationData(f"Lightning Gem #{i + 1}", "Lightning Gem", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(2)] +
|
||||
[DS3LocationData(f"Deep Gem #{i + 1}", "Deep Gem", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(2)] +
|
||||
[DS3LocationData(f"Dark Gem #{i + 1}", "Dark Gem", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(2)] +
|
||||
[DS3LocationData(f"Poison Gem #{i + 1}", "Poison Gem", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(2)] +
|
||||
[DS3LocationData(f"Blood Gem #{i + 1}", "Blood Gem", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(1)] +
|
||||
[DS3LocationData(f"Raw Gem #{i + 1}", "Raw Gem", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(2)] +
|
||||
[DS3LocationData(f"Blessed Gem #{i + 1}", "Blessed Gem", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(3)] +
|
||||
[DS3LocationData(f"Hollow Gem #{i + 1}", "Hollow Gem", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(2)] +
|
||||
[DS3LocationData(f"Shriving Stone #{i + 1}", "Shriving Stone", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(3)],
|
||||
|
||||
"Progressive Items DLC": [] +
|
||||
# Upgrade materials
|
||||
[DS3LocationData(f"Large Titanite Shard ${i + 1}", "Large Titanite Shard", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(3)] +
|
||||
[DS3LocationData(f"Titanite Chunk ${i + 1}", "Titanite Chunk", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(15)] +
|
||||
[DS3LocationData(f"Titanite Slab ${i + 1}", "Titanite Slab", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(2)] +
|
||||
[DS3LocationData(f"Twinkling Titanite ${i + 1}", "Twinkling Titanite", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(5)] +
|
||||
[DS3LocationData(f"Titanite Scale ${i + 1}", "Titanite Scale", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(11)] +
|
||||
|
||||
|
||||
# Items
|
||||
[DS3LocationData(f"Homeward Bone ${i + 1}", "Homeward Bone", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(6)] +
|
||||
[DS3LocationData(f"Rusted Coin ${i + 1}", "Rusted Coin", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(3)] +
|
||||
[DS3LocationData(f"Ember ${i + 1}", "Ember", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(10)] +
|
||||
|
||||
# Souls
|
||||
[DS3LocationData(f"Large Soul of an Unknown Traveler ${i + 1}", "Large Soul of an Unknown Traveler", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(9)] +
|
||||
[DS3LocationData(f"Soul of a Weary Warrior ${i + 1}", "Soul of a Weary Warrior", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(5)] +
|
||||
[DS3LocationData(f"Large Soul of a Weary Warrior ${i + 1}", "Large Soul of a Weary Warrior", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(6)] +
|
||||
[DS3LocationData(f"Soul of a Crestfallen Knight ${i + 1}", "Soul of a Crestfallen Knight", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(6)] +
|
||||
[DS3LocationData(f"Large Soul of a Crestfallen Knight ${i + 1}", "Large Soul of a Crestfallen Knight", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(3)] +
|
||||
|
||||
# Gems
|
||||
[DS3LocationData(f"Dark Gem ${i + 1}", "Dark Gem", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(2)] +
|
||||
[DS3LocationData(f"Blood Gem ${i + 1}", "Blood Gem", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(1)] +
|
||||
[DS3LocationData(f"Blessed Gem ${i + 1}", "Blessed Gem", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(2)] +
|
||||
[DS3LocationData(f"Hollow Gem ${i + 1}", "Hollow Gem", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(2)]
|
||||
}
|
||||
|
||||
location_dictionary: Dict[str, DS3LocationData] = {}
|
||||
for location_table in location_tables.values():
|
||||
location_dictionary.update({location_data.name: location_data for location_data in location_table})
|
||||
|
||||
@@ -1,16 +1,91 @@
|
||||
import typing
|
||||
from Options import Toggle, Option, Range, Choice, DeathLink
|
||||
|
||||
from Options import Toggle, DefaultOnToggle, Option, Range, Choice, ItemDict, DeathLink
|
||||
|
||||
|
||||
class RandomizeWeaponLocations(DefaultOnToggle):
|
||||
"""Randomizes weapons (+76 locations)"""
|
||||
display_name = "Randomize Weapon Locations"
|
||||
|
||||
|
||||
class RandomizeShieldLocations(DefaultOnToggle):
|
||||
"""Randomizes shields (+24 locations)"""
|
||||
display_name = "Randomize Shield Locations"
|
||||
|
||||
|
||||
class RandomizeArmorLocations(DefaultOnToggle):
|
||||
"""Randomizes armor pieces (+97 locations)"""
|
||||
display_name = "Randomize Armor Locations"
|
||||
|
||||
|
||||
class RandomizeRingLocations(DefaultOnToggle):
|
||||
"""Randomizes rings (+49 locations)"""
|
||||
display_name = "Randomize Ring Locations"
|
||||
|
||||
|
||||
class RandomizeSpellLocations(DefaultOnToggle):
|
||||
"""Randomizes spells (+18 locations)"""
|
||||
display_name = "Randomize Spell Locations"
|
||||
|
||||
|
||||
class RandomizeKeyLocations(DefaultOnToggle):
|
||||
"""Randomizes items which unlock doors or bypass barriers"""
|
||||
display_name = "Randomize Key Locations"
|
||||
|
||||
|
||||
class RandomizeBossSoulLocations(DefaultOnToggle):
|
||||
"""Randomizes Boss Souls (+18 Locations)"""
|
||||
display_name = "Randomize Boss Soul Locations"
|
||||
|
||||
|
||||
class RandomizeNPCLocations(Toggle):
|
||||
"""Randomizes friendly NPC drops (meaning you will probably have to kill them) (+14 locations)"""
|
||||
display_name = "Randomize NPC Locations"
|
||||
|
||||
|
||||
class RandomizeMiscLocations(Toggle):
|
||||
"""Randomizes miscellaneous items (ashes, tomes, scrolls, etc.) to the pool. (+36 locations)"""
|
||||
display_name = "Randomize Miscellaneous Locations"
|
||||
|
||||
|
||||
class RandomizeHealthLocations(Toggle):
|
||||
"""Randomizes health upgrade items. (+21 locations)"""
|
||||
display_name = "Randomize Health Upgrade Locations"
|
||||
|
||||
|
||||
class RandomizeProgressiveLocationsOption(Toggle):
|
||||
"""Randomizes upgrade materials and consumables such as the titanite shards, firebombs, resin, etc...
|
||||
|
||||
Instead of specific locations, these are progressive, so Titanite Shard #1 is the first titanite shard
|
||||
you pick up, regardless of whether it's from an enemy drop late in the game or an item on the ground in the
|
||||
first 5 minutes."""
|
||||
display_name = "Randomize Progressive Locations"
|
||||
|
||||
|
||||
class PoolTypeOption(Choice):
|
||||
"""Changes which non-progression items you add to the pool
|
||||
|
||||
Shuffle: Items are picked from the locations being randomized
|
||||
Various: Items are picked from a list of all items in the game, but are the same type of item they replace"""
|
||||
display_name = "Pool Type"
|
||||
option_shuffle = 0
|
||||
option_various = 1
|
||||
|
||||
|
||||
class GuaranteedItemsOption(ItemDict):
|
||||
"""Guarantees that the specified items will be in the item pool"""
|
||||
display_name = "Guaranteed Items"
|
||||
|
||||
|
||||
class AutoEquipOption(Toggle):
|
||||
"""Automatically equips any received armor or left/right weapons."""
|
||||
display_name = "Auto-equip"
|
||||
display_name = "Auto-Equip"
|
||||
|
||||
|
||||
class LockEquipOption(Toggle):
|
||||
"""Lock the equipment slots so you cannot change your armor or your left/right weapons. Works great with the
|
||||
Auto-equip option."""
|
||||
display_name = "Lock Equipement Slots"
|
||||
display_name = "Lock Equipment Slots"
|
||||
|
||||
|
||||
class NoWeaponRequirementsOption(Toggle):
|
||||
@@ -26,93 +101,124 @@ class NoSpellRequirementsOption(Toggle):
|
||||
|
||||
class NoEquipLoadOption(Toggle):
|
||||
"""Disable the equip load constraint from the game"""
|
||||
display_name = "No Equip load"
|
||||
display_name = "No Equip Load"
|
||||
|
||||
|
||||
class RandomizeWeaponsLevelOption(Choice):
|
||||
class RandomizeInfusionOption(Toggle):
|
||||
"""Enable this option to infuse a percentage of the pool of weapons and shields."""
|
||||
display_name = "Randomize Infusion"
|
||||
|
||||
|
||||
class RandomizeInfusionPercentageOption(Range):
|
||||
"""The percentage of weapons/shields in the pool to be infused if Randomize Infusion is toggled"""
|
||||
display_name = "Percentage of Infused Weapons"
|
||||
range_start = 0
|
||||
range_end = 100
|
||||
default = 33
|
||||
|
||||
|
||||
class RandomizeWeaponLevelOption(Choice):
|
||||
"""Enable this option to upgrade a percentage of the pool of weapons to a random value between the minimum and
|
||||
maximum levels defined.
|
||||
all: All weapons are eligible, both basic and epic
|
||||
basic: Only weapons that can be upgraded to +10
|
||||
epic: Only weapons that can be upgraded to +5"""
|
||||
display_name = "Randomize weapons level"
|
||||
maximum levels defined.
|
||||
|
||||
All: All weapons are eligible, both basic and epic
|
||||
Basic: Only weapons that can be upgraded to +10
|
||||
Epic: Only weapons that can be upgraded to +5"""
|
||||
display_name = "Randomize Weapon Level"
|
||||
option_none = 0
|
||||
option_all = 1
|
||||
option_basic = 2
|
||||
option_epic = 3
|
||||
|
||||
|
||||
class RandomizeWeaponsLevelPercentageOption(Range):
|
||||
class RandomizeWeaponLevelPercentageOption(Range):
|
||||
"""The percentage of weapons in the pool to be upgraded if randomize weapons level is toggled"""
|
||||
display_name = "Percentage of randomized weapons"
|
||||
range_start = 1
|
||||
display_name = "Percentage of Randomized Weapons"
|
||||
range_start = 0
|
||||
range_end = 100
|
||||
default = 33
|
||||
|
||||
|
||||
class MinLevelsIn5WeaponPoolOption(Range):
|
||||
"""The minimum upgraded value of a weapon in the pool of weapons that can only reach +5"""
|
||||
display_name = "Minimum level of +5 weapons"
|
||||
range_start = 1
|
||||
display_name = "Minimum Level of +5 Weapons"
|
||||
range_start = 0
|
||||
range_end = 5
|
||||
default = 1
|
||||
|
||||
|
||||
class MaxLevelsIn5WeaponPoolOption(Range):
|
||||
"""The maximum upgraded value of a weapon in the pool of weapons that can only reach +5"""
|
||||
display_name = "Maximum level of +5 weapons"
|
||||
range_start = 1
|
||||
display_name = "Maximum Level of +5 Weapons"
|
||||
range_start = 0
|
||||
range_end = 5
|
||||
default = 5
|
||||
|
||||
|
||||
class MinLevelsIn10WeaponPoolOption(Range):
|
||||
"""The minimum upgraded value of a weapon in the pool of weapons that can reach +10"""
|
||||
display_name = "Minimum level of +10 weapons"
|
||||
range_start = 1
|
||||
display_name = "Minimum Level of +10 Weapons"
|
||||
range_start = 0
|
||||
range_end = 10
|
||||
default = 1
|
||||
|
||||
|
||||
class MaxLevelsIn10WeaponPoolOption(Range):
|
||||
"""The maximum upgraded value of a weapon in the pool of weapons that can reach +10"""
|
||||
display_name = "Maximum level of +10 weapons"
|
||||
range_start = 1
|
||||
display_name = "Maximum Level of +10 Weapons"
|
||||
range_start = 0
|
||||
range_end = 10
|
||||
default = 10
|
||||
|
||||
|
||||
class LateBasinOfVowsOption(Toggle):
|
||||
"""Force the Basin of Vows to be located as a reward of defeating Pontiff Sulyvahn. It permits to ease the
|
||||
progression by preventing having to kill the Dancer of the Boreal Valley as the first boss"""
|
||||
"""This option makes it so the Basin of Vows is still randomized, but guarantees you that you wont have to venture into
|
||||
Lothric Castle to find your Small Lothric Banner to get out of High Wall of Lothric. So you may find Basin of Vows early,
|
||||
but you wont have to fight Dancer to find your Small Lothric Banner."""
|
||||
display_name = "Late Basin of Vows"
|
||||
|
||||
|
||||
class EnableProgressiveLocationsOption(Toggle):
|
||||
"""Randomize upgrade materials such as the titanite shards, the estus shards and the consumables"""
|
||||
display_name = "Randomize materials, Estus shards and consumables (+196 checks/items)"
|
||||
class LateDLCOption(Toggle):
|
||||
"""This option makes it so you are guaranteed to find your Small Doll without having to venture off into the DLC,
|
||||
effectively putting anything in the DLC in logic after finding both Contraption Key and Small Doll,
|
||||
and being able to get into Irithyll of the Boreal Valley."""
|
||||
display_name = "Late DLC"
|
||||
|
||||
|
||||
class EnableDLCOption(Toggle):
|
||||
"""To use this option, you must own both the ASHES OF ARIANDEL and the RINGED CITY DLC"""
|
||||
display_name = "Add the DLC Items and Locations to the pool (+81 checks/items)"
|
||||
display_name = "Enable DLC"
|
||||
|
||||
|
||||
dark_souls_options: typing.Dict[str, type(Option)] = {
|
||||
dark_souls_options: typing.Dict[str, Option] = {
|
||||
"enable_weapon_locations": RandomizeWeaponLocations,
|
||||
"enable_shield_locations": RandomizeShieldLocations,
|
||||
"enable_armor_locations": RandomizeArmorLocations,
|
||||
"enable_ring_locations": RandomizeRingLocations,
|
||||
"enable_spell_locations": RandomizeSpellLocations,
|
||||
"enable_key_locations": RandomizeKeyLocations,
|
||||
"enable_boss_locations": RandomizeBossSoulLocations,
|
||||
"enable_npc_locations": RandomizeNPCLocations,
|
||||
"enable_misc_locations": RandomizeMiscLocations,
|
||||
"enable_health_upgrade_locations": RandomizeHealthLocations,
|
||||
"enable_progressive_locations": RandomizeProgressiveLocationsOption,
|
||||
"pool_type": PoolTypeOption,
|
||||
"guaranteed_items": GuaranteedItemsOption,
|
||||
"auto_equip": AutoEquipOption,
|
||||
"lock_equip": LockEquipOption,
|
||||
"no_weapon_requirements": NoWeaponRequirementsOption,
|
||||
"randomize_weapons_level": RandomizeWeaponsLevelOption,
|
||||
"randomize_weapons_percentage": RandomizeWeaponsLevelPercentageOption,
|
||||
"randomize_infusion": RandomizeInfusionOption,
|
||||
"randomize_infusion_percentage": RandomizeInfusionPercentageOption,
|
||||
"randomize_weapon_level": RandomizeWeaponLevelOption,
|
||||
"randomize_weapon_level_percentage": RandomizeWeaponLevelPercentageOption,
|
||||
"min_levels_in_5": MinLevelsIn5WeaponPoolOption,
|
||||
"max_levels_in_5": MaxLevelsIn5WeaponPoolOption,
|
||||
"min_levels_in_10": MinLevelsIn10WeaponPoolOption,
|
||||
"max_levels_in_10": MaxLevelsIn10WeaponPoolOption,
|
||||
"late_basin_of_vows": LateBasinOfVowsOption,
|
||||
"late_dlc": LateDLCOption,
|
||||
"no_spell_requirements": NoSpellRequirementsOption,
|
||||
"no_equip_load": NoEquipLoadOption,
|
||||
"death_link": DeathLink,
|
||||
"enable_progressive_locations": EnableProgressiveLocationsOption,
|
||||
"enable_dlc": EnableDLCOption,
|
||||
}
|
||||
|
||||
|
||||
@@ -1,21 +1,15 @@
|
||||
# world/dark_souls_3/__init__.py
|
||||
from typing import Dict
|
||||
from typing import Dict, Set, List
|
||||
|
||||
from .Items import DarkSouls3Item
|
||||
from .Locations import DarkSouls3Location
|
||||
from .Options import dark_souls_options
|
||||
from .data.items_data import weapons_upgrade_5_table, weapons_upgrade_10_table, item_dictionary, key_items_list, \
|
||||
dlc_weapons_upgrade_5_table, dlc_weapons_upgrade_10_table
|
||||
from .data.locations_data import location_dictionary, fire_link_shrine_table, \
|
||||
high_wall_of_lothric, \
|
||||
undead_settlement_table, road_of_sacrifice_table, consumed_king_garden_table, cathedral_of_the_deep_table, \
|
||||
farron_keep_table, catacombs_of_carthus_table, smouldering_lake_table, irithyll_of_the_boreal_valley_table, \
|
||||
irithyll_dungeon_table, profaned_capital_table, anor_londo_table, lothric_castle_table, grand_archives_table, \
|
||||
untended_graves_table, archdragon_peak_table, firelink_shrine_bell_tower_table, progressive_locations, \
|
||||
progressive_locations_2, progressive_locations_3, painted_world_table, dreg_heap_table, ringed_city_table, dlc_progressive_locations
|
||||
from ..AutoWorld import World, WebWorld
|
||||
from BaseClasses import MultiWorld, Region, Item, Entrance, Tutorial, ItemClassification
|
||||
from ..generic.Rules import set_rule, add_item_rule
|
||||
from Options import Toggle
|
||||
|
||||
from worlds.AutoWorld import World, WebWorld
|
||||
from worlds.generic.Rules import set_rule, add_rule, add_item_rule
|
||||
|
||||
from .Items import DarkSouls3Item, DS3ItemCategory, item_dictionary, key_item_names
|
||||
from .Locations import DarkSouls3Location, DS3LocationCategory, location_tables, location_dictionary
|
||||
from .Options import RandomizeWeaponLevelOption, PoolTypeOption, dark_souls_options
|
||||
|
||||
|
||||
class DarkSouls3Web(WebWorld):
|
||||
@@ -52,212 +46,399 @@ class DarkSouls3World(World):
|
||||
option_definitions = dark_souls_options
|
||||
topology_present: bool = True
|
||||
web = DarkSouls3Web()
|
||||
data_version = 5
|
||||
data_version = 7
|
||||
base_id = 100000
|
||||
required_client_version = (0, 3, 7)
|
||||
enabled_location_categories: Set[DS3LocationCategory]
|
||||
required_client_version = (0, 4, 2)
|
||||
item_name_to_id = DarkSouls3Item.get_name_to_id()
|
||||
location_name_to_id = DarkSouls3Location.get_name_to_id()
|
||||
|
||||
def __init__(self, world: MultiWorld, player: int):
|
||||
super().__init__(world, player)
|
||||
|
||||
def __init__(self, multiworld: MultiWorld, player: int):
|
||||
super().__init__(multiworld, player)
|
||||
self.locked_items = []
|
||||
self.locked_locations = []
|
||||
self.main_path_locations = []
|
||||
self.enabled_location_categories = set()
|
||||
|
||||
|
||||
def generate_early(self):
|
||||
if self.multiworld.enable_weapon_locations[self.player] == Toggle.option_true:
|
||||
self.enabled_location_categories.add(DS3LocationCategory.WEAPON)
|
||||
if self.multiworld.enable_shield_locations[self.player] == Toggle.option_true:
|
||||
self.enabled_location_categories.add(DS3LocationCategory.SHIELD)
|
||||
if self.multiworld.enable_armor_locations[self.player] == Toggle.option_true:
|
||||
self.enabled_location_categories.add(DS3LocationCategory.ARMOR)
|
||||
if self.multiworld.enable_ring_locations[self.player] == Toggle.option_true:
|
||||
self.enabled_location_categories.add(DS3LocationCategory.RING)
|
||||
if self.multiworld.enable_spell_locations[self.player] == Toggle.option_true:
|
||||
self.enabled_location_categories.add(DS3LocationCategory.SPELL)
|
||||
if self.multiworld.enable_npc_locations[self.player] == Toggle.option_true:
|
||||
self.enabled_location_categories.add(DS3LocationCategory.NPC)
|
||||
if self.multiworld.enable_key_locations[self.player] == Toggle.option_true:
|
||||
self.enabled_location_categories.add(DS3LocationCategory.KEY)
|
||||
if self.multiworld.enable_boss_locations[self.player] == Toggle.option_true:
|
||||
self.enabled_location_categories.add(DS3LocationCategory.BOSS)
|
||||
if self.multiworld.enable_misc_locations[self.player] == Toggle.option_true:
|
||||
self.enabled_location_categories.add(DS3LocationCategory.MISC)
|
||||
if self.multiworld.enable_health_upgrade_locations[self.player] == Toggle.option_true:
|
||||
self.enabled_location_categories.add(DS3LocationCategory.HEALTH)
|
||||
if self.multiworld.enable_progressive_locations[self.player] == Toggle.option_true:
|
||||
self.enabled_location_categories.add(DS3LocationCategory.PROGRESSIVE_ITEM)
|
||||
|
||||
|
||||
def create_regions(self):
|
||||
progressive_location_table = []
|
||||
if self.multiworld.enable_progressive_locations[self.player].value:
|
||||
progressive_location_table = [] + \
|
||||
location_tables["Progressive Items 1"] + \
|
||||
location_tables["Progressive Items 2"] + \
|
||||
location_tables["Progressive Items 3"] + \
|
||||
location_tables["Progressive Items 4"]
|
||||
|
||||
if self.multiworld.enable_dlc[self.player].value:
|
||||
progressive_location_table += location_tables["Progressive Items DLC"]
|
||||
|
||||
# Create Vanilla Regions
|
||||
regions: Dict[str, Region] = {}
|
||||
regions["Menu"] = self.create_region("Menu", progressive_location_table)
|
||||
regions.update({region_name: self.create_region(region_name, location_tables[region_name]) for region_name in [
|
||||
"Firelink Shrine",
|
||||
"Firelink Shrine Bell Tower",
|
||||
"High Wall of Lothric",
|
||||
"Undead Settlement",
|
||||
"Road of Sacrifices",
|
||||
"Cathedral of the Deep",
|
||||
"Farron Keep",
|
||||
"Catacombs of Carthus",
|
||||
"Smouldering Lake",
|
||||
"Irithyll of the Boreal Valley",
|
||||
"Irithyll Dungeon",
|
||||
"Profaned Capital",
|
||||
"Anor Londo",
|
||||
"Lothric Castle",
|
||||
"Consumed King's Garden",
|
||||
"Grand Archives",
|
||||
"Untended Graves",
|
||||
"Archdragon Peak",
|
||||
"Kiln of the First Flame",
|
||||
]})
|
||||
|
||||
# Adds Path of the Dragon as an event item for Archdragon Peak access
|
||||
potd_location = DarkSouls3Location(self.player, "CKG: Path of the Dragon", DS3LocationCategory.EVENT, "Path of the Dragon", None, regions["Consumed King's Garden"])
|
||||
potd_location.place_locked_item(Item("Path of the Dragon", ItemClassification.progression, None, self.player))
|
||||
regions["Consumed King's Garden"].locations.append(potd_location)
|
||||
|
||||
# Create DLC Regions
|
||||
if self.multiworld.enable_dlc[self.player]:
|
||||
regions.update({region_name: self.create_region(region_name, location_tables[region_name]) for region_name in [
|
||||
"Painted World of Ariandel 1",
|
||||
"Painted World of Ariandel 2",
|
||||
"Dreg Heap",
|
||||
"Ringed City",
|
||||
]})
|
||||
|
||||
# Connect Regions
|
||||
def create_connection(from_region: str, to_region: str):
|
||||
connection = Entrance(self.player, f"Go To {to_region}", regions[from_region])
|
||||
regions[from_region].exits.append(connection)
|
||||
connection.connect(regions[to_region])
|
||||
|
||||
regions["Menu"].exits.append(Entrance(self.player, "New Game", regions["Menu"]))
|
||||
self.multiworld.get_entrance("New Game", self.player).connect(regions["Firelink Shrine"])
|
||||
|
||||
create_connection("Firelink Shrine", "High Wall of Lothric")
|
||||
create_connection("Firelink Shrine", "Firelink Shrine Bell Tower")
|
||||
create_connection("Firelink Shrine", "Kiln of the First Flame")
|
||||
|
||||
create_connection("High Wall of Lothric", "Undead Settlement")
|
||||
create_connection("High Wall of Lothric", "Lothric Castle")
|
||||
|
||||
create_connection("Undead Settlement", "Road of Sacrifices")
|
||||
|
||||
create_connection("Road of Sacrifices", "Cathedral of the Deep")
|
||||
create_connection("Road of Sacrifices", "Farron Keep")
|
||||
|
||||
create_connection("Farron Keep", "Catacombs of Carthus")
|
||||
|
||||
create_connection("Catacombs of Carthus", "Irithyll of the Boreal Valley")
|
||||
create_connection("Catacombs of Carthus", "Smouldering Lake")
|
||||
|
||||
create_connection("Irithyll of the Boreal Valley", "Irithyll Dungeon")
|
||||
create_connection("Irithyll of the Boreal Valley", "Anor Londo")
|
||||
|
||||
create_connection("Irithyll Dungeon", "Archdragon Peak")
|
||||
create_connection("Irithyll Dungeon", "Profaned Capital")
|
||||
|
||||
create_connection("Lothric Castle", "Consumed King's Garden")
|
||||
create_connection("Lothric Castle", "Grand Archives")
|
||||
|
||||
create_connection("Consumed King's Garden", "Untended Graves")
|
||||
|
||||
# Connect DLC Regions
|
||||
if self.multiworld.enable_dlc[self.player]:
|
||||
create_connection("Cathedral of the Deep", "Painted World of Ariandel 1")
|
||||
create_connection("Painted World of Ariandel 1", "Painted World of Ariandel 2")
|
||||
create_connection("Painted World of Ariandel 2", "Dreg Heap")
|
||||
create_connection("Dreg Heap", "Ringed City")
|
||||
|
||||
|
||||
# For each region, add the associated locations retrieved from the corresponding location_table
|
||||
def create_region(self, region_name, location_table) -> Region:
|
||||
new_region = Region(region_name, self.player, self.multiworld)
|
||||
|
||||
for location in location_table:
|
||||
if location.category in self.enabled_location_categories:
|
||||
new_location = DarkSouls3Location(
|
||||
self.player,
|
||||
location.name,
|
||||
location.category,
|
||||
location.default_item,
|
||||
self.location_name_to_id[location.name],
|
||||
new_region
|
||||
)
|
||||
else:
|
||||
# Replace non-randomized progression items with events
|
||||
event_item = self.create_item(location.default_item)
|
||||
if event_item.classification != ItemClassification.progression:
|
||||
continue
|
||||
|
||||
new_location = DarkSouls3Location(
|
||||
self.player,
|
||||
location.name,
|
||||
location.category,
|
||||
location.default_item,
|
||||
None,
|
||||
new_region
|
||||
)
|
||||
event_item.code = None
|
||||
new_location.place_locked_item(event_item)
|
||||
|
||||
if region_name == "Menu":
|
||||
add_item_rule(new_location, lambda item: not item.advancement)
|
||||
|
||||
new_region.locations.append(new_location)
|
||||
|
||||
self.multiworld.regions.append(new_region)
|
||||
return new_region
|
||||
|
||||
|
||||
def create_items(self):
|
||||
dlc_enabled = self.multiworld.enable_dlc[self.player] == Toggle.option_true
|
||||
|
||||
itempool_by_category = {category: [] for category in self.enabled_location_categories}
|
||||
|
||||
# Gather all default items on randomized locations
|
||||
num_required_extra_items = 0
|
||||
for location in self.multiworld.get_locations(self.player):
|
||||
if location.category in itempool_by_category:
|
||||
if item_dictionary[location.default_item_name].category == DS3ItemCategory.SKIP:
|
||||
num_required_extra_items += 1
|
||||
else:
|
||||
itempool_by_category[location.category].append(location.default_item_name)
|
||||
|
||||
# Replace each item category with a random sample of items of those types
|
||||
if self.multiworld.pool_type[self.player] == PoolTypeOption.option_various:
|
||||
def create_random_replacement_list(item_categories: Set[DS3ItemCategory], num_items: int):
|
||||
candidates = [
|
||||
item.name for item
|
||||
in item_dictionary.values()
|
||||
if (item.category in item_categories and (not item.is_dlc or dlc_enabled))
|
||||
]
|
||||
return self.multiworld.random.sample(candidates, num_items)
|
||||
|
||||
if DS3LocationCategory.WEAPON in self.enabled_location_categories:
|
||||
itempool_by_category[DS3LocationCategory.WEAPON] = create_random_replacement_list(
|
||||
{
|
||||
DS3ItemCategory.WEAPON_UPGRADE_5,
|
||||
DS3ItemCategory.WEAPON_UPGRADE_10,
|
||||
DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE
|
||||
},
|
||||
len(itempool_by_category[DS3LocationCategory.WEAPON])
|
||||
)
|
||||
if DS3LocationCategory.SHIELD in self.enabled_location_categories:
|
||||
itempool_by_category[DS3LocationCategory.SHIELD] = create_random_replacement_list(
|
||||
{DS3ItemCategory.SHIELD, DS3ItemCategory.SHIELD_INFUSIBLE},
|
||||
len(itempool_by_category[DS3LocationCategory.SHIELD])
|
||||
)
|
||||
if DS3LocationCategory.ARMOR in self.enabled_location_categories:
|
||||
itempool_by_category[DS3LocationCategory.ARMOR] = create_random_replacement_list(
|
||||
{DS3ItemCategory.ARMOR},
|
||||
len(itempool_by_category[DS3LocationCategory.ARMOR])
|
||||
)
|
||||
if DS3LocationCategory.RING in self.enabled_location_categories:
|
||||
itempool_by_category[DS3LocationCategory.RING] = create_random_replacement_list(
|
||||
{DS3ItemCategory.RING},
|
||||
len(itempool_by_category[DS3LocationCategory.RING])
|
||||
)
|
||||
if DS3LocationCategory.SPELL in self.enabled_location_categories:
|
||||
itempool_by_category[DS3LocationCategory.SPELL] = create_random_replacement_list(
|
||||
{DS3ItemCategory.SPELL},
|
||||
len(itempool_by_category[DS3LocationCategory.SPELL])
|
||||
)
|
||||
|
||||
itempool: List[DarkSouls3Item] = []
|
||||
for category in self.enabled_location_categories:
|
||||
itempool += [self.create_item(name) for name in itempool_by_category[category]]
|
||||
|
||||
# A list of items we can replace
|
||||
removable_items = [item for item in itempool if item.classification != ItemClassification.progression]
|
||||
|
||||
guaranteed_items = self.multiworld.guaranteed_items[self.player].value
|
||||
for item_name in guaranteed_items:
|
||||
# Break early just in case nothing is removable (if user is trying to guarantee more
|
||||
# items than the pool can hold, for example)
|
||||
if len(removable_items) == 0:
|
||||
break
|
||||
|
||||
num_existing_copies = len([item for item in itempool if item.name == item_name])
|
||||
for _ in range(guaranteed_items[item_name]):
|
||||
if num_existing_copies > 0:
|
||||
num_existing_copies -= 1
|
||||
continue
|
||||
|
||||
if num_required_extra_items > 0:
|
||||
# We can just add them instead of using "Soul of an Intrepid Hero" later
|
||||
num_required_extra_items -= 1
|
||||
else:
|
||||
if len(removable_items) == 0:
|
||||
break
|
||||
|
||||
# Try to construct a list of items with the same category that can be removed
|
||||
# If none exist, just remove something at random
|
||||
removable_shortlist = [
|
||||
item for item
|
||||
in removable_items
|
||||
if item_dictionary[item.name].category == item_dictionary[item_name].category
|
||||
]
|
||||
if len(removable_shortlist) == 0:
|
||||
removable_shortlist = removable_items
|
||||
|
||||
removed_item = self.multiworld.random.choice(removable_shortlist)
|
||||
removable_items.remove(removed_item) # To avoid trying to replace the same item twice
|
||||
itempool.remove(removed_item)
|
||||
|
||||
itempool.append(self.create_item(item_name))
|
||||
|
||||
# Extra filler items for locations containing SKIP items
|
||||
itempool += [self.create_filler() for _ in range(num_required_extra_items)]
|
||||
|
||||
# Add items to itempool
|
||||
self.multiworld.itempool += itempool
|
||||
|
||||
|
||||
def create_item(self, name: str) -> Item:
|
||||
useful_categories = {
|
||||
DS3ItemCategory.WEAPON_UPGRADE_5,
|
||||
DS3ItemCategory.WEAPON_UPGRADE_10,
|
||||
DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE,
|
||||
DS3ItemCategory.SPELL,
|
||||
}
|
||||
data = self.item_name_to_id[name]
|
||||
|
||||
if name in key_items_list:
|
||||
if name in key_item_names:
|
||||
item_classification = ItemClassification.progression
|
||||
elif name in weapons_upgrade_5_table or name in weapons_upgrade_10_table:
|
||||
elif item_dictionary[name].category in useful_categories or name in {"Estus Shard", "Undead Bone Shard"}:
|
||||
item_classification = ItemClassification.useful
|
||||
else:
|
||||
item_classification = ItemClassification.filler
|
||||
|
||||
return DarkSouls3Item(name, item_classification, data, self.player)
|
||||
|
||||
def create_regions(self):
|
||||
|
||||
if self.multiworld.enable_progressive_locations[self.player].value and self.multiworld.enable_dlc[self.player].value:
|
||||
menu_region = self.create_region("Menu", {**progressive_locations, **progressive_locations_2,
|
||||
**progressive_locations_3, **dlc_progressive_locations})
|
||||
elif self.multiworld.enable_progressive_locations[self.player].value:
|
||||
menu_region = self.create_region("Menu", {**progressive_locations, **progressive_locations_2,
|
||||
**progressive_locations_3})
|
||||
else:
|
||||
menu_region = self.create_region("Menu", None)
|
||||
def get_filler_item_name(self) -> str:
|
||||
return "Soul of an Intrepid Hero"
|
||||
|
||||
# Create all Vanilla regions of Dark Souls III
|
||||
firelink_shrine_region = self.create_region("Firelink Shrine", fire_link_shrine_table)
|
||||
firelink_shrine_bell_tower_region = self.create_region("Firelink Shrine Bell Tower",
|
||||
firelink_shrine_bell_tower_table)
|
||||
high_wall_of_lothric_region = self.create_region("High Wall of Lothric", high_wall_of_lothric)
|
||||
undead_settlement_region = self.create_region("Undead Settlement", undead_settlement_table)
|
||||
road_of_sacrifices_region = self.create_region("Road of Sacrifices", road_of_sacrifice_table)
|
||||
consumed_king_garden_region = self.create_region("Consumed King's Garden", consumed_king_garden_table)
|
||||
cathedral_of_the_deep_region = self.create_region("Cathedral of the Deep", cathedral_of_the_deep_table)
|
||||
farron_keep_region = self.create_region("Farron Keep", farron_keep_table)
|
||||
catacombs_of_carthus_region = self.create_region("Catacombs of Carthus", catacombs_of_carthus_table)
|
||||
smouldering_lake_region = self.create_region("Smouldering Lake", smouldering_lake_table)
|
||||
irithyll_of_the_boreal_valley_region = self.create_region("Irithyll of the Boreal Valley",
|
||||
irithyll_of_the_boreal_valley_table)
|
||||
irithyll_dungeon_region = self.create_region("Irithyll Dungeon", irithyll_dungeon_table)
|
||||
profaned_capital_region = self.create_region("Profaned Capital", profaned_capital_table)
|
||||
anor_londo_region = self.create_region("Anor Londo", anor_londo_table)
|
||||
lothric_castle_region = self.create_region("Lothric Castle", lothric_castle_table)
|
||||
grand_archives_region = self.create_region("Grand Archives", grand_archives_table)
|
||||
untended_graves_region = self.create_region("Untended Graves", untended_graves_table)
|
||||
archdragon_peak_region = self.create_region("Archdragon Peak", archdragon_peak_table)
|
||||
kiln_of_the_first_flame_region = self.create_region("Kiln Of The First Flame", None)
|
||||
# DLC Down here
|
||||
if self.multiworld.enable_dlc[self.player]:
|
||||
painted_world_of_ariandel_region = self.create_region("Painted World of Ariandel", painted_world_table)
|
||||
dreg_heap_region = self.create_region("Dreg Heap", dreg_heap_table)
|
||||
ringed_city_region = self.create_region("Ringed City", ringed_city_table)
|
||||
|
||||
# Create the entrance to connect those regions
|
||||
menu_region.exits.append(Entrance(self.player, "New Game", menu_region))
|
||||
self.multiworld.get_entrance("New Game", self.player).connect(firelink_shrine_region)
|
||||
firelink_shrine_region.exits.append(Entrance(self.player, "Goto High Wall of Lothric",
|
||||
firelink_shrine_region))
|
||||
firelink_shrine_region.exits.append(Entrance(self.player, "Goto Kiln Of The First Flame",
|
||||
firelink_shrine_region))
|
||||
firelink_shrine_region.exits.append(Entrance(self.player, "Goto Bell Tower",
|
||||
firelink_shrine_region))
|
||||
self.multiworld.get_entrance("Goto High Wall of Lothric", self.player).connect(high_wall_of_lothric_region)
|
||||
self.multiworld.get_entrance("Goto Kiln Of The First Flame", self.player).connect(
|
||||
kiln_of_the_first_flame_region)
|
||||
self.multiworld.get_entrance("Goto Bell Tower", self.player).connect(firelink_shrine_bell_tower_region)
|
||||
high_wall_of_lothric_region.exits.append(Entrance(self.player, "Goto Undead Settlement",
|
||||
high_wall_of_lothric_region))
|
||||
high_wall_of_lothric_region.exits.append(Entrance(self.player, "Goto Lothric Castle",
|
||||
high_wall_of_lothric_region))
|
||||
self.multiworld.get_entrance("Goto Undead Settlement", self.player).connect(undead_settlement_region)
|
||||
self.multiworld.get_entrance("Goto Lothric Castle", self.player).connect(lothric_castle_region)
|
||||
undead_settlement_region.exits.append(Entrance(self.player, "Goto Road Of Sacrifices",
|
||||
undead_settlement_region))
|
||||
self.multiworld.get_entrance("Goto Road Of Sacrifices", self.player).connect(road_of_sacrifices_region)
|
||||
road_of_sacrifices_region.exits.append(Entrance(self.player, "Goto Cathedral", road_of_sacrifices_region))
|
||||
road_of_sacrifices_region.exits.append(Entrance(self.player, "Goto Farron keep", road_of_sacrifices_region))
|
||||
self.multiworld.get_entrance("Goto Cathedral", self.player).connect(cathedral_of_the_deep_region)
|
||||
self.multiworld.get_entrance("Goto Farron keep", self.player).connect(farron_keep_region)
|
||||
farron_keep_region.exits.append(Entrance(self.player, "Goto Carthus catacombs", farron_keep_region))
|
||||
self.multiworld.get_entrance("Goto Carthus catacombs", self.player).connect(catacombs_of_carthus_region)
|
||||
catacombs_of_carthus_region.exits.append(Entrance(self.player, "Goto Irithyll of the boreal",
|
||||
catacombs_of_carthus_region))
|
||||
catacombs_of_carthus_region.exits.append(Entrance(self.player, "Goto Smouldering Lake",
|
||||
catacombs_of_carthus_region))
|
||||
self.multiworld.get_entrance("Goto Irithyll of the boreal", self.player). \
|
||||
connect(irithyll_of_the_boreal_valley_region)
|
||||
self.multiworld.get_entrance("Goto Smouldering Lake", self.player).connect(smouldering_lake_region)
|
||||
irithyll_of_the_boreal_valley_region.exits.append(Entrance(self.player, "Goto Irithyll dungeon",
|
||||
irithyll_of_the_boreal_valley_region))
|
||||
irithyll_of_the_boreal_valley_region.exits.append(Entrance(self.player, "Goto Anor Londo",
|
||||
irithyll_of_the_boreal_valley_region))
|
||||
self.multiworld.get_entrance("Goto Irithyll dungeon", self.player).connect(irithyll_dungeon_region)
|
||||
self.multiworld.get_entrance("Goto Anor Londo", self.player).connect(anor_londo_region)
|
||||
irithyll_dungeon_region.exits.append(Entrance(self.player, "Goto Archdragon peak", irithyll_dungeon_region))
|
||||
irithyll_dungeon_region.exits.append(Entrance(self.player, "Goto Profaned capital", irithyll_dungeon_region))
|
||||
self.multiworld.get_entrance("Goto Archdragon peak", self.player).connect(archdragon_peak_region)
|
||||
self.multiworld.get_entrance("Goto Profaned capital", self.player).connect(profaned_capital_region)
|
||||
lothric_castle_region.exits.append(Entrance(self.player, "Goto Consumed King Garden", lothric_castle_region))
|
||||
lothric_castle_region.exits.append(Entrance(self.player, "Goto Grand Archives", lothric_castle_region))
|
||||
self.multiworld.get_entrance("Goto Consumed King Garden", self.player).connect(consumed_king_garden_region)
|
||||
self.multiworld.get_entrance("Goto Grand Archives", self.player).connect(grand_archives_region)
|
||||
consumed_king_garden_region.exits.append(Entrance(self.player, "Goto Untended Graves",
|
||||
consumed_king_garden_region))
|
||||
self.multiworld.get_entrance("Goto Untended Graves", self.player).connect(untended_graves_region)
|
||||
# DLC Connectors Below
|
||||
if self.multiworld.enable_dlc[self.player]:
|
||||
cathedral_of_the_deep_region.exits.append(Entrance(self.player, "Goto Painted World of Ariandel",
|
||||
cathedral_of_the_deep_region))
|
||||
self.multiworld.get_entrance("Goto Painted World of Ariandel", self.player).connect(painted_world_of_ariandel_region)
|
||||
painted_world_of_ariandel_region.exits.append(Entrance(self.player, "Goto Dreg Heap",
|
||||
painted_world_of_ariandel_region))
|
||||
self.multiworld.get_entrance("Goto Dreg Heap", self.player).connect(dreg_heap_region)
|
||||
dreg_heap_region.exits.append(Entrance(self.player, "Goto Ringed City", dreg_heap_region))
|
||||
self.multiworld.get_entrance("Goto Ringed City", self.player).connect(ringed_city_region)
|
||||
|
||||
# For each region, add the associated locations retrieved from the corresponding location_table
|
||||
def create_region(self, region_name, location_table) -> Region:
|
||||
new_region = Region(region_name, self.player, self.multiworld)
|
||||
if location_table:
|
||||
for name, address in location_table.items():
|
||||
location = DarkSouls3Location(self.player, name, self.location_name_to_id[name], new_region)
|
||||
if region_name == "Menu":
|
||||
add_item_rule(location, lambda item: not item.advancement)
|
||||
new_region.locations.append(location)
|
||||
self.multiworld.regions.append(new_region)
|
||||
return new_region
|
||||
|
||||
def create_items(self):
|
||||
for name, address in self.item_name_to_id.items():
|
||||
# Specific items will be included in the item pool under certain conditions. See generate_basic
|
||||
if name == "Basin of Vows":
|
||||
continue
|
||||
# Do not add progressive_items ( containing "#" ) to the itempool if the option is disabled
|
||||
if (not self.multiworld.enable_progressive_locations[self.player]) and "#" in name:
|
||||
continue
|
||||
# Do not add DLC items if the option is disabled
|
||||
if (not self.multiworld.enable_dlc[self.player]) and DarkSouls3Item.is_dlc_item(name):
|
||||
continue
|
||||
# Do not add DLC Progressives if both options are disabled
|
||||
if ((not self.multiworld.enable_progressive_locations[self.player]) or (not self.multiworld.enable_dlc[self.player])) and DarkSouls3Item.is_dlc_progressive(name):
|
||||
continue
|
||||
self.multiworld.itempool += [self.create_item(name)]
|
||||
|
||||
def generate_early(self):
|
||||
pass
|
||||
|
||||
def set_rules(self) -> None:
|
||||
|
||||
# Define the access rules to the entrances
|
||||
set_rule(self.multiworld.get_entrance("Goto Bell Tower", self.player),
|
||||
lambda state: state.has("Tower Key", self.player))
|
||||
set_rule(self.multiworld.get_entrance("Goto Undead Settlement", self.player),
|
||||
set_rule(self.multiworld.get_entrance("Go To Undead Settlement", self.player),
|
||||
lambda state: state.has("Small Lothric Banner", self.player))
|
||||
set_rule(self.multiworld.get_entrance("Goto Lothric Castle", self.player),
|
||||
set_rule(self.multiworld.get_entrance("Go To Lothric Castle", self.player),
|
||||
lambda state: state.has("Basin of Vows", self.player))
|
||||
set_rule(self.multiworld.get_entrance("Goto Irithyll of the boreal", self.player),
|
||||
set_rule(self.multiworld.get_entrance("Go To Irithyll of the Boreal Valley", self.player),
|
||||
lambda state: state.has("Small Doll", self.player))
|
||||
set_rule(self.multiworld.get_entrance("Goto Archdragon peak", self.player),
|
||||
lambda state: state.can_reach("CKG: Soul of Consumed Oceiros", "Location", self.player))
|
||||
set_rule(self.multiworld.get_entrance("Goto Profaned capital", self.player),
|
||||
lambda state: state.has("Storm Ruler", self.player))
|
||||
set_rule(self.multiworld.get_entrance("Goto Grand Archives", self.player),
|
||||
set_rule(self.multiworld.get_entrance("Go To Archdragon Peak", self.player),
|
||||
lambda state: state.has("Path of the Dragon", self.player))
|
||||
set_rule(self.multiworld.get_entrance("Go To Grand Archives", self.player),
|
||||
lambda state: state.has("Grand Archives Key", self.player))
|
||||
set_rule(self.multiworld.get_entrance("Goto Kiln Of The First Flame", self.player),
|
||||
set_rule(self.multiworld.get_entrance("Go To Kiln of the First Flame", self.player),
|
||||
lambda state: state.has("Cinders of a Lord - Abyss Watcher", self.player) and
|
||||
state.has("Cinders of a Lord - Yhorm the Giant", self.player) and
|
||||
state.has("Cinders of a Lord - Aldrich", self.player) and
|
||||
state.has("Cinders of a Lord - Lothric Prince", self.player))
|
||||
|
||||
if self.multiworld.late_basin_of_vows[self.player] == Toggle.option_true:
|
||||
add_rule(self.multiworld.get_entrance("Go To Lothric Castle", self.player),
|
||||
lambda state: state.has("Small Lothric Banner", self.player))
|
||||
|
||||
# DLC Access Rules Below
|
||||
if self.multiworld.enable_dlc[self.player]:
|
||||
set_rule(self.multiworld.get_entrance("Goto Painted World of Ariandel", self.player),
|
||||
lambda state: state.has("Contraption Key", self.player))
|
||||
set_rule(self.multiworld.get_entrance("Goto Ringed City", self.player),
|
||||
set_rule(self.multiworld.get_entrance("Go To Ringed City", self.player),
|
||||
lambda state: state.has("Small Envoy Banner", self.player))
|
||||
|
||||
# If key items are randomized, must have contraption key to enter second half of Ashes DLC
|
||||
# If key items are not randomized, Contraption Key is guaranteed to be accessible before it is needed
|
||||
if self.multiworld.enable_key_locations[self.player] == Toggle.option_true:
|
||||
add_rule(self.multiworld.get_entrance("Go To Painted World of Ariandel 2", self.player),
|
||||
lambda state: state.has("Contraption Key", self.player))
|
||||
|
||||
if self.multiworld.late_dlc[self.player] == Toggle.option_true:
|
||||
add_rule(self.multiworld.get_entrance("Go To Painted World of Ariandel 1", self.player),
|
||||
lambda state: state.has("Small Doll", self.player))
|
||||
|
||||
# Define the access rules to some specific locations
|
||||
set_rule(self.multiworld.get_location("HWL: Soul of the Dancer", self.player),
|
||||
lambda state: state.has("Basin of Vows", self.player))
|
||||
set_rule(self.multiworld.get_location("HWL: Greirat's Ashes", self.player),
|
||||
lambda state: state.has("Cell Key", self.player))
|
||||
set_rule(self.multiworld.get_location("HWL: Blue Tearstone Ring", self.player),
|
||||
lambda state: state.has("Cell Key", self.player))
|
||||
set_rule(self.multiworld.get_location("ID: Bellowing Dragoncrest Ring", self.player),
|
||||
lambda state: state.has("Jailbreaker's Key", self.player))
|
||||
set_rule(self.multiworld.get_location("ID: Prisoner Chief's Ashes", self.player),
|
||||
lambda state: state.has("Jailer's Key Ring", self.player))
|
||||
set_rule(self.multiworld.get_location("ID: Covetous Gold Serpent Ring", self.player),
|
||||
lambda state: state.has("Old Cell Key", self.player))
|
||||
set_rule(self.multiworld.get_location("ID: Karla's Ashes", self.player),
|
||||
lambda state: state.has("Jailer's Key Ring", self.player))
|
||||
black_hand_gotthard_corpse_rule = lambda state: \
|
||||
set_rule(self.multiworld.get_location("PC: Cinders of a Lord - Yhorm the Giant", self.player),
|
||||
lambda state: state.has("Storm Ruler", self.player))
|
||||
|
||||
if self.multiworld.enable_ring_locations[self.player] == Toggle.option_true:
|
||||
set_rule(self.multiworld.get_location("ID: Bellowing Dragoncrest Ring", self.player),
|
||||
lambda state: state.has("Jailbreaker's Key", self.player))
|
||||
set_rule(self.multiworld.get_location("ID: Covetous Gold Serpent Ring", self.player),
|
||||
lambda state: state.has("Old Cell Key", self.player))
|
||||
set_rule(self.multiworld.get_location("UG: Hornet Ring", self.player),
|
||||
lambda state: state.has("Small Lothric Banner", self.player))
|
||||
|
||||
if self.multiworld.enable_npc_locations[self.player] == Toggle.option_true:
|
||||
set_rule(self.multiworld.get_location("HWL: Greirat's Ashes", self.player),
|
||||
lambda state: state.has("Cell Key", self.player))
|
||||
set_rule(self.multiworld.get_location("HWL: Blue Tearstone Ring", self.player),
|
||||
lambda state: state.has("Cell Key", self.player))
|
||||
set_rule(self.multiworld.get_location("ID: Karla's Ashes", self.player),
|
||||
lambda state: state.has("Jailer's Key Ring", self.player))
|
||||
set_rule(self.multiworld.get_location("ID: Karla's Pointed Hat", self.player),
|
||||
lambda state: state.has("Jailer's Key Ring", self.player))
|
||||
set_rule(self.multiworld.get_location("ID: Karla's Coat", self.player),
|
||||
lambda state: state.has("Jailer's Key Ring", self.player))
|
||||
set_rule(self.multiworld.get_location("ID: Karla's Gloves", self.player),
|
||||
lambda state: state.has("Jailer's Key Ring", self.player))
|
||||
set_rule(self.multiworld.get_location("ID: Karla's Trousers", self.player),
|
||||
lambda state: state.has("Jailer's Key Ring", self.player))
|
||||
|
||||
if self.multiworld.enable_misc_locations[self.player] == Toggle.option_true:
|
||||
set_rule(self.multiworld.get_location("ID: Prisoner Chief's Ashes", self.player),
|
||||
lambda state: state.has("Jailer's Key Ring", self.player))
|
||||
|
||||
if self.multiworld.enable_boss_locations[self.player] == Toggle.option_true:
|
||||
set_rule(self.multiworld.get_location("PC: Soul of Yhorm the Giant", self.player),
|
||||
lambda state: state.has("Storm Ruler", self.player))
|
||||
set_rule(self.multiworld.get_location("HWL: Soul of the Dancer", self.player),
|
||||
lambda state: state.has("Basin of Vows", self.player))
|
||||
|
||||
# Lump Soul of the Dancer in with LC for locations that should not be reachable
|
||||
# before having access to US. (Prevents requiring getting Basin to fight Dancer to get SLB to go to US)
|
||||
if self.multiworld.late_basin_of_vows[self.player] == Toggle.option_true:
|
||||
add_rule(self.multiworld.get_location("HWL: Soul of the Dancer", self.player),
|
||||
lambda state: state.has("Small Lothric Banner", self.player))
|
||||
|
||||
gotthard_corpse_rule = lambda state: \
|
||||
(state.can_reach("AL: Cinders of a Lord - Aldrich", "Location", self.player) and
|
||||
state.can_reach("PC: Cinders of a Lord - Yhorm the Giant", "Location", self.player))
|
||||
set_rule(self.multiworld.get_location("LC: Grand Archives Key", self.player), black_hand_gotthard_corpse_rule)
|
||||
set_rule(self.multiworld.get_location("LC: Gotthard Twinswords", self.player), black_hand_gotthard_corpse_rule)
|
||||
|
||||
set_rule(self.multiworld.get_location("LC: Grand Archives Key", self.player), gotthard_corpse_rule)
|
||||
|
||||
if self.multiworld.enable_weapon_locations[self.player] == Toggle.option_true:
|
||||
set_rule(self.multiworld.get_location("LC: Gotthard Twinswords", self.player), gotthard_corpse_rule)
|
||||
|
||||
self.multiworld.completion_condition[self.player] = lambda state: \
|
||||
state.has("Cinders of a Lord - Abyss Watcher", self.player) and \
|
||||
@@ -265,57 +446,36 @@ class DarkSouls3World(World):
|
||||
state.has("Cinders of a Lord - Aldrich", self.player) and \
|
||||
state.has("Cinders of a Lord - Lothric Prince", self.player)
|
||||
|
||||
def generate_basic(self):
|
||||
# Depending on the specified option, add the Basin of Vows to a specific location or to the item pool
|
||||
item = self.create_item("Basin of Vows")
|
||||
if self.multiworld.late_basin_of_vows[self.player]:
|
||||
self.multiworld.get_location("IBV: Soul of Pontiff Sulyvahn", self.player).place_locked_item(item)
|
||||
else:
|
||||
self.multiworld.itempool += [item]
|
||||
|
||||
# Fill item pool with additional items
|
||||
item_pool_len = self.item_name_to_id.__len__()
|
||||
total_required_locations = self.location_name_to_id.__len__()
|
||||
for i in range(item_pool_len, total_required_locations):
|
||||
self.multiworld.itempool += [self.create_item("Soul of an Intrepid Hero")]
|
||||
|
||||
def fill_slot_data(self) -> Dict[str, object]:
|
||||
slot_data: Dict[str, object] = {}
|
||||
|
||||
# Depending on the specified option, modify items hexadecimal value to add an upgrade level
|
||||
item_dictionary_copy = item_dictionary.copy()
|
||||
if self.multiworld.randomize_weapons_level[self.player] > 0:
|
||||
# Depending on the specified option, modify items hexadecimal value to add an upgrade level or infusion
|
||||
name_to_ds3_code = {item.name: item.ds3_code for item in item_dictionary.values()}
|
||||
|
||||
# Randomize some weapon upgrades
|
||||
if self.multiworld.randomize_weapon_level[self.player] != RandomizeWeaponLevelOption.option_none:
|
||||
# if the user made an error and set a min higher than the max we default to the max
|
||||
max_5 = self.multiworld.max_levels_in_5[self.player]
|
||||
min_5 = min(self.multiworld.min_levels_in_5[self.player], max_5)
|
||||
max_10 = self.multiworld.max_levels_in_10[self.player]
|
||||
min_10 = min(self.multiworld.min_levels_in_10[self.player], max_10)
|
||||
weapons_percentage = self.multiworld.randomize_weapons_percentage[self.player]
|
||||
weapon_level_percentage = self.multiworld.randomize_weapon_level_percentage[self.player]
|
||||
|
||||
# Randomize some weapons upgrades
|
||||
if self.multiworld.randomize_weapons_level[self.player] in [1, 3]: # Options are either all or +5
|
||||
for name in weapons_upgrade_5_table.keys():
|
||||
if self.multiworld.per_slot_randoms[self.player].randint(1, 100) < weapons_percentage:
|
||||
value = self.multiworld.per_slot_randoms[self.player].randint(min_5, max_5)
|
||||
item_dictionary_copy[name] += value
|
||||
for item in item_dictionary.values():
|
||||
if self.multiworld.per_slot_randoms[self.player].randint(0, 99) < weapon_level_percentage:
|
||||
if item.category == DS3ItemCategory.WEAPON_UPGRADE_5:
|
||||
name_to_ds3_code[item.name] += self.multiworld.per_slot_randoms[self.player].randint(min_5, max_5)
|
||||
elif item.category in {DS3ItemCategory.WEAPON_UPGRADE_10, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE}:
|
||||
name_to_ds3_code[item.name] += self.multiworld.per_slot_randoms[self.player].randint(min_10, max_10)
|
||||
|
||||
if self.multiworld.randomize_weapons_level[self.player] in [1, 2]: # Options are either all or +10
|
||||
for name in weapons_upgrade_10_table.keys():
|
||||
if self.multiworld.per_slot_randoms[self.player].randint(1, 100) < weapons_percentage:
|
||||
value = self.multiworld.per_slot_randoms[self.player].randint(min_10, max_10)
|
||||
item_dictionary_copy[name] += value
|
||||
|
||||
if self.multiworld.randomize_weapons_level[self.player] in [1, 3]:
|
||||
for name in dlc_weapons_upgrade_5_table.keys():
|
||||
if self.multiworld.per_slot_randoms[self.player].randint(1, 100) < weapons_percentage:
|
||||
value = self.multiworld.per_slot_randoms[self.player].randint(min_5, max_5)
|
||||
item_dictionary_copy[name] += value
|
||||
|
||||
if self.multiworld.randomize_weapons_level[self.player] in [1, 2]:
|
||||
for name in dlc_weapons_upgrade_10_table.keys():
|
||||
if self.multiworld.per_slot_randoms[self.player].randint(1, 100) < weapons_percentage:
|
||||
value = self.multiworld.per_slot_randoms[self.player].randint(min_10, max_10)
|
||||
item_dictionary_copy[name] += value
|
||||
# Randomize some weapon infusions
|
||||
if self.multiworld.randomize_infusion[self.player] == Toggle.option_true:
|
||||
infusion_percentage = self.multiworld.randomize_infusion_percentage[self.player]
|
||||
for item in item_dictionary.values():
|
||||
if item.category in {DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE, DS3ItemCategory.SHIELD_INFUSIBLE}:
|
||||
if self.multiworld.per_slot_randoms[self.player].randint(0, 99) < infusion_percentage:
|
||||
name_to_ds3_code[item.name] += 100 * self.multiworld.per_slot_randoms[self.player].randint(0, 15)
|
||||
|
||||
# Create the mandatory lists to generate the player's output file
|
||||
items_id = []
|
||||
@@ -324,15 +484,19 @@ class DarkSouls3World(World):
|
||||
locations_address = []
|
||||
locations_target = []
|
||||
for location in self.multiworld.get_filled_locations():
|
||||
# Skip events
|
||||
if location.item.code is None:
|
||||
continue
|
||||
|
||||
if location.item.player == self.player:
|
||||
items_id.append(location.item.code)
|
||||
items_address.append(item_dictionary_copy[location.item.name])
|
||||
items_address.append(name_to_ds3_code[location.item.name])
|
||||
|
||||
if location.player == self.player:
|
||||
locations_address.append(location_dictionary[location.name])
|
||||
locations_address.append(item_dictionary[location_dictionary[location.name].default_item].ds3_code)
|
||||
locations_id.append(location.address)
|
||||
if location.item.player == self.player:
|
||||
locations_target.append(item_dictionary_copy[location.item.name])
|
||||
locations_target.append(name_to_ds3_code[location.item.name])
|
||||
else:
|
||||
locations_target.append(0)
|
||||
|
||||
|
||||
@@ -1,600 +0,0 @@
|
||||
"""
|
||||
Tools used to create this list :
|
||||
List of all items https://docs.google.com/spreadsheets/d/1nK2g7g6XJ-qphFAk1tjP3jZtlXWDQY-ItKLa_sniawo/edit#gid=1551945791
|
||||
Regular expression parser https://regex101.com/r/XdtiLR/2
|
||||
List of locations https://darksouls3.wiki.fextralife.com/Locations
|
||||
"""
|
||||
|
||||
weapons_upgrade_5_table = {
|
||||
"Irithyll Straight Sword": 0x0020A760,
|
||||
"Chaos Blade": 0x004C9960,
|
||||
"Dragonrider Bow": 0x00D6B0F0,
|
||||
"White Hair Talisman": 0x00CAF120,
|
||||
"Izalith Staff": 0x00C96A80,
|
||||
"Fume Ultra Greatsword": 0x0060E4B0,
|
||||
"Black Knight Sword": 0x005F5E10,
|
||||
"Yorshka's Spear": 0x008C3A70,
|
||||
"Smough's Great Hammer": 0x007E30B0,
|
||||
"Dragonslayer Greatbow": 0x00CF8500,
|
||||
"Golden Ritual Spear": 0x00C83200,
|
||||
"Eleonora": 0x006CCB90,
|
||||
"Witch's Locks": 0x00B7B740,
|
||||
"Crystal Chime": 0x00CA2DD0,
|
||||
"Black Knight Glaive": 0x009AE070,
|
||||
"Dragonslayer Spear": 0x008CAFA0,
|
||||
"Caitha's Chime": 0x00CA06C0,
|
||||
"Sunlight Straight Sword": 0x00203230,
|
||||
"Firelink Greatsword": 0x0060BDA0,
|
||||
"Hollowslayer Greatsword": 0x00604870,
|
||||
"Arstor's Spear": 0x008BEC50,
|
||||
"Vordt's Great Hammer": 0x007CD120,
|
||||
"Crystal Sage's Rapier": 0x002E6300,
|
||||
"Farron Greatsword": 0x005E9AC0,
|
||||
"Wolf Knight's Greatsword": 0x00602160,
|
||||
"Dancer's Enchanted Swords": 0x00F4C040,
|
||||
"Wolnir's Holy Sword": 0x005FFA50,
|
||||
"Demon's Greataxe": 0x006CA480,
|
||||
"Demon's Fist": 0x00A84DF0,
|
||||
"Old King's Great Hammer": 0x007CF830,
|
||||
"Greatsword of Judgment": 0x005E2590,
|
||||
"Profaned Greatsword": 0x005E4CA0,
|
||||
"Yhorm's Great Machete": 0x005F0FF0,
|
||||
"Cleric's Candlestick": 0x0020F580,
|
||||
"Dragonslayer Greataxe": 0x006C7D70,
|
||||
"Moonlight Greatsword": 0x00606F80,
|
||||
"Gundyr's Halberd": 0x009A1D20,
|
||||
"Lothric's Holy Sword": 0x005FD340,
|
||||
"Lorian's Greatsword": 0x005F8520,
|
||||
"Twin Princes' Greatsword": 0x005FAC30,
|
||||
"Storm Curved Sword": 0x003E4180,
|
||||
"Dragonslayer Swordspear": 0x008BC540,
|
||||
"Sage's Crystal Staff": 0x00C8CE40,
|
||||
"Irithyll Rapier": 0x002E8A10
|
||||
}
|
||||
|
||||
dlc_weapons_upgrade_5_table = {
|
||||
"Friede's Great Scythe": 0x009B55A0,
|
||||
"Rose of Ariandel": 0x00B82C70,
|
||||
"Demon's Scar": 0x003F04D0, # Assigned to "RC: Church Guardian Shiv"
|
||||
"Frayed Blade": 0x004D35A0, # Assigned to "RC: Ritual Spear Fragment"
|
||||
"Gael's Greatsword": 0x00227C20, # Assigned to "RC: Violet Wrappings"
|
||||
"Repeating Crossbow": 0x00D885B0, # Assigned to "RC: Blood of the Dark Souls"
|
||||
"Onyx Blade": 0x00222E00, # VILHELM FIGHT
|
||||
"Earth Seeker": 0x006D8EE0,
|
||||
"Quakestone Hammer": 0x007ECCF0,
|
||||
"Millwood Greatbow": 0x00D85EA0,
|
||||
"Valorheart": 0x00F646E0,
|
||||
"Aquamarine Dagger": 0x00116520,
|
||||
"Ringed Knight Straight Sword": 0x00225510,
|
||||
"Ledo's Great Hammer": 0x007EF400, # INVADER FIGHT
|
||||
"Ringed Knight Spear": 0x008CFDC0,
|
||||
"Crucifix of the Mad King": 0x008D4BE0,
|
||||
"Sacred Chime of Filianore": 0x00CCECF0,
|
||||
"Preacher's Right Arm": 0x00CD1400,
|
||||
"White Birch Bow": 0x00D77440,
|
||||
"Ringed Knight Paired Greatswords": 0x00F69500
|
||||
}
|
||||
|
||||
weapons_upgrade_10_table = {
|
||||
"Broken Straight Sword": 0x001EF9B0,
|
||||
"Deep Battle Axe": 0x0006AFA54,
|
||||
"Club": 0x007A1200,
|
||||
"Claymore": 0x005BDBA0,
|
||||
"Longbow": 0x00D689E0,
|
||||
"Mail Breaker": 0x002DEDD0,
|
||||
"Broadsword": 0x001ED2A0,
|
||||
"Astora's Straight Sword": 0x002191C0,
|
||||
"Rapier": 0x002E14E0,
|
||||
"Lucerne": 0x0098BD90,
|
||||
"Whip": 0x00B71B00,
|
||||
"Reinforced Club": 0x007A8730,
|
||||
"Caestus": 0x00A7FFD0,
|
||||
"Partizan": 0x0089C970,
|
||||
"Red Hilted Halberd": 0x009AB960,
|
||||
"Saint's Talisman": 0x00CACA10,
|
||||
"Large Club": 0x007AFC60,
|
||||
"Brigand Twindaggers": 0x00F50E60,
|
||||
"Butcher Knife": 0x006BE130,
|
||||
"Brigand Axe": 0x006B1DE0,
|
||||
"Heretic's Staff": 0x00C8F550,
|
||||
"Great Club": 0x007B4A80,
|
||||
"Exile Greatsword": 0x005DD770,
|
||||
"Sellsword Twinblades": 0x00F42400,
|
||||
"Notched Whip": 0x00B7DE50,
|
||||
"Astora Greatsword": 0x005C9EF0,
|
||||
"Executioner's Greatsword": 0x0021DFE0,
|
||||
"Saint-tree Bellvine": 0x00C9DFB0,
|
||||
"Saint Bident": 0x008C1360,
|
||||
"Drang Hammers": 0x00F61FD0,
|
||||
"Arbalest": 0x00D662D0,
|
||||
"Sunlight Talisman": 0x00CA54E0,
|
||||
"Greatsword": 0x005C50D0,
|
||||
"Black Bow of Pharis": 0x00D7E970,
|
||||
"Great Axe": 0x006B9310,
|
||||
"Black Blade": 0x004CC070,
|
||||
"Blacksmith Hammer": 0x007E57C0,
|
||||
"Witchtree Branch": 0x00C94370,
|
||||
"Painting Guardian's Curved Sword": 0x003E6890,
|
||||
"Pickaxe": 0x007DE290,
|
||||
"Court Sorcerer's Staff": 0x00C91C60,
|
||||
"Avelyn": 0x00D6FF10,
|
||||
"Onikiri and Ubadachi": 0x00F58390,
|
||||
"Ricard's Rapier": 0x002E3BF0,
|
||||
"Drakeblood Greatsword": 0x00609690,
|
||||
"Greatlance": 0x008A8CC0,
|
||||
"Sniper Crossbow": 0x00D83790,
|
||||
"Claw": 0x00A7D8C0,
|
||||
"Drang Twinspears": 0x00F5AAA0,
|
||||
"Pyromancy Flame": 0x00CC77C0 #given/dropped by Cornyx
|
||||
}
|
||||
|
||||
dlc_weapons_upgrade_10_table = {
|
||||
"Follower Sabre": 0x003EDDC0,
|
||||
"Millwood Battle Axe": 0x006D67D0,
|
||||
"Follower Javelin": 0x008CD6B0,
|
||||
"Crow Talons": 0x00A89C10,
|
||||
"Pyromancer's Parting Flame": 0x00CC9ED0,
|
||||
"Crow Quills": 0x00F66DF0,
|
||||
"Follower Torch": 0x015F1AD0,
|
||||
"Murky Hand Scythe": 0x00118C30,
|
||||
"Herald Curved Greatsword": 0x006159E0,
|
||||
"Lothric War Banner": 0x008D24D0,
|
||||
"Splitleaf Greatsword": 0x009B2E90, # SHOP ITEM
|
||||
"Murky Longstaff": 0x00CCC5E0,
|
||||
}
|
||||
|
||||
shields_table = {
|
||||
"East-West Shield": 0x0142B930,
|
||||
"Silver Eagle Kite Shield": 0x014418C0,
|
||||
"Small Leather Shield": 0x01315410,
|
||||
"Blue Wooden Shield": 0x0143F1B0,
|
||||
"Plank Shield": 0x01346150,
|
||||
"Caduceus Round Shield": 0x01341330,
|
||||
"Wargod Wooden Shield": 0x0144DC10,
|
||||
"Grass Crest Shield": 0x01437C80,
|
||||
"Golden Falcon Shield": 0x01354BB0,
|
||||
"Twin Dragon Greatshield": 0x01513820,
|
||||
"Spider Shield": 0x01435570,
|
||||
"Crest Shield": 0x01430750,
|
||||
"Curse Ward Greatshield": 0x01518640,
|
||||
"Stone Parma": 0x01443FD0,
|
||||
"Dragon Crest Shield": 0x01432E60,
|
||||
"Shield of Want": 0x0144B500,
|
||||
"Black Iron Greatshield": 0x0150EA00,
|
||||
"Greatshield of Glory": 0x01515F30,
|
||||
"Sacred Bloom Shield": 0x013572C0,
|
||||
"Golden Wing Crest Shield": 0x0143CAA0,
|
||||
"Ancient Dragon Greatshield": 0x013599D0,
|
||||
"Spirit Tree Crest Shield": 0x014466E0,
|
||||
"Blessed Red and White Shield": 0x01343FB9
|
||||
}
|
||||
|
||||
dlc_shields_table = {
|
||||
"Followers Shield": 0x0135C0E0,
|
||||
"Ethereal Oak Shield": 0x01450320,
|
||||
"Giant Door Shield": 0x00F5F8C0,
|
||||
"Dragonhead Shield": 0x0135E7F0,
|
||||
"Dragonhead Greatshield": 0x01452A30
|
||||
}
|
||||
|
||||
goods_table = {
|
||||
"Soul of an Intrepid Hero": 0x4000019D,
|
||||
"Soul of the Nameless King": 0x400002D2,
|
||||
"Soul of Champion Gundyr": 0x400002C8,
|
||||
"Soul of the Twin Princes": 0x400002DB,
|
||||
"Soul of Consumed Oceiros": 0x400002CE,
|
||||
"Soul of Aldrich": 0x400002D5,
|
||||
"Soul of Yhorm the Giant": 0x400002DC,
|
||||
"Soul of Pontiff Sulyvahn": 0x400002D4,
|
||||
"Soul of the Old Demon King": 0x400002D0,
|
||||
"Soul of High Lord Wolnir": 0x400002D6,
|
||||
"Soul of the Blood of the Wolf": 0x400002CD,
|
||||
"Soul of the Deacons of the Deep": 0x400002D9,
|
||||
"Soul of a Crystal Sage": 0x400002CB,
|
||||
"Soul of Boreal Valley Vordt": 0x400002CF,
|
||||
"Soul of a Stray Demon": 0x400002E7,
|
||||
"Soul of a Demon": 0x400002E3,
|
||||
|
||||
# Upgrade materials
|
||||
**{"Titanite Shard #"+str(i): 0x400003E8 for i in range(1, 11)},
|
||||
**{"Large Titanite Shard #"+str(i): 0x400003E9 for i in range(1, 11)},
|
||||
**{"Titanite Chunk #"+str(i): 0x400003EA for i in range(1, 6)},
|
||||
**{"Titanite Slab #"+str(i): 0x400003EB for i in range(1, 4)},
|
||||
|
||||
# Healing
|
||||
**{"Estus Shard #"+str(i): 0x4000085D for i in range(1, 16)},
|
||||
**{"Undead Bone Shard #"+str(i): 0x4000085F for i in range(1, 6)},
|
||||
|
||||
# Souls
|
||||
**{"Soul of a Great Champion #"+str(i): 0x400001A4 for i in range(1, 3)},
|
||||
**{"Soul of a Champion #"+str(i): 0x400001A3 for i in range(1, 5)},
|
||||
**{"Soul of a Venerable Old Hand #"+str(i): 0x400001A2 for i in range(1, 5)},
|
||||
**{"Soul of a Crestfallen Knight #"+str(i): 0x40000199 for i in range(1, 11)}
|
||||
}
|
||||
|
||||
goods_2_table = { # Added by Br00ty
|
||||
"HWL: Gold Pine Resin #": 0x4000014B,
|
||||
"US: Charcoal Pine Resin #": 0x4000014A,
|
||||
"FK: Gold Pine Bundle #": 0x40000155,
|
||||
"CC: Carthus Rouge #": 0x4000014F,
|
||||
"ID: Pale Pine Resin #": 0x40000150,
|
||||
**{"Ember #"+str(i): 0x400001F4 for i in range(1, 45)},
|
||||
**{"Titanite Shard #"+str(i): 0x400003E8 for i in range(11, 16)},
|
||||
**{"Large Titanite Shard #"+str(i): 0x400003E9 for i in range(11, 16)},
|
||||
**{"Titanite Scale #" + str(i): 0x400003FC for i in range(1, 25)}
|
||||
}
|
||||
|
||||
goods_3_table = { # Added by Br00ty
|
||||
**{"Fading Soul #" + str(i): 0x40000190 for i in range(1, 4)},
|
||||
**{"Ring of Sacrifice #"+str(i): 0x20004EF2 for i in range(1, 5)},
|
||||
**{"Homeward Bone #"+str(i): 0x4000015E for i in range(1, 17)},
|
||||
**{"Green Blossom #"+str(i): 0x40000104 for i in range(1, 7)},
|
||||
**{"Human Pine Resin #"+str(i): 0x4000014E for i in range(1, 3)},
|
||||
**{"Charcoal Pine Bundle #"+str(i): 0x40000154 for i in range(1, 3)},
|
||||
**{"Rotten Pine Resin #"+str(i): 0x40000157 for i in range(1, 3)},
|
||||
**{"Alluring Skull #"+str(i): 0x40000126 for i in range(1, 9)},
|
||||
**{"Rusted Coin #"+str(i): 0x400001C7 for i in range(1, 3)},
|
||||
**{"Rusted Gold Coin #"+str(i): 0x400001C9 for i in range(1, 3)},
|
||||
**{"Titanite Chunk #"+str(i): 0x400003EA for i in range(1, 17)},
|
||||
**{"Twinkling Titanite #"+str(i): 0x40000406 for i in range(1, 8)}
|
||||
}
|
||||
|
||||
dlc_goods_table = {
|
||||
"Soul of Sister Friede": 0x400002E8,
|
||||
"Soul of the Demon Prince": 0x400002EA,
|
||||
"Soul of Darkeater Midir": 0x400002EB,
|
||||
"Soul of Slave Knight Gael": 0x400002E9
|
||||
}
|
||||
|
||||
dlc_goods_2_table = { #71
|
||||
**{"Large Soul of an Unknown Traveler $"+str(i): 0x40000194 for i in range(1, 10)},
|
||||
**{"Soul of a Weary Warrior $"+str(i): 0x40000197 for i in range(1, 6)},
|
||||
**{"Large Soul of a Weary Warrior $"+str(i): 0x40000198 for i in range(1, 7)},
|
||||
**{"Soul of a Crestfallen Knight $"+str(i): 0x40000199 for i in range(1, 7)},
|
||||
**{"Large Soul of a Crestfallen Knight $"+str(i): 0x4000019A for i in range(1, 4)},
|
||||
**{"Homeward Bone $"+str(i): 0x4000015E for i in range(1, 7)},
|
||||
**{"Large Titanite Shard $"+str(i): 0x400003E9 for i in range(1, 4)},
|
||||
**{"Titanite Chunk $"+str(i): 0x400003EA for i in range(1, 16)},
|
||||
**{"Twinkling Titanite $"+str(i): 0x40000406 for i in range(1, 6)},
|
||||
**{"Rusted Coin $"+str(i): 0x400001C7 for i in range(1, 4)},
|
||||
**{"Ember $"+str(i): 0x400001F4 for i in range(1, 11)}
|
||||
}
|
||||
|
||||
armor_table = {
|
||||
"Fire Keeper Robe": 0x140D9CE8,
|
||||
"Fire Keeper Gloves": 0x140DA0D0,
|
||||
"Fire Keeper Skirt": 0x140DA4B8,
|
||||
"Deserter Trousers": 0x126265B8,
|
||||
"Cleric Hat": 0x11D905C0,
|
||||
"Cleric Blue Robe": 0x11D909A8,
|
||||
"Cleric Gloves": 0x11D90D90,
|
||||
"Cleric Trousers": 0x11D91178,
|
||||
"Northern Helm": 0x116E3600,
|
||||
"Northern Armor": 0x116E39E8,
|
||||
"Northern Gloves": 0x116E3DD0,
|
||||
"Northern Trousers": 0x116E41B8,
|
||||
"Loincloth": 0x148F57D8,
|
||||
|
||||
"Brigand Hood": 0x148009E0,
|
||||
"Brigand Armor": 0x14800DC8,
|
||||
"Brigand Gauntlets": 0x148011B0,
|
||||
"Brigand Trousers": 0x14801598,
|
||||
"Sorcerer Hood": 0x11C9C380,
|
||||
"Sorcerer Robe": 0x11C9C768,
|
||||
"Sorcerer Gloves": 0x11C9CB50,
|
||||
"Sorcerer Trousers": 0x11C9CF38,
|
||||
"Fallen Knight Helm": 0x1121EAC0,
|
||||
"Fallen Knight Armor": 0x1121EEA8,
|
||||
"Fallen Knight Gauntlets": 0x1121F290,
|
||||
"Fallen Knight Trousers": 0x1121F678,
|
||||
"Conjurator Hood": 0x149E8E60,
|
||||
"Conjurator Robe": 0x149E9248,
|
||||
"Conjurator Manchettes": 0x149E9630,
|
||||
"Conjurator Boots": 0x149E9A18,
|
||||
|
||||
"Sellsword Helm": 0x11481060,
|
||||
"Sellsword Armor": 0x11481448,
|
||||
"Sellsword Gauntlet": 0x11481830,
|
||||
"Sellsword Trousers": 0x11481C18,
|
||||
"Herald Helm": 0x114FB180,
|
||||
"Herald Armor": 0x114FB568,
|
||||
"Herald Gloves": 0x114FB950,
|
||||
"Herald Trousers": 0x114FBD38,
|
||||
|
||||
"Maiden Hood": 0x14BD12E0,
|
||||
"Maiden Robe": 0x14BD16C8,
|
||||
"Maiden Gloves": 0x14BD1AB0,
|
||||
"Maiden Skirt": 0x14BD1E98,
|
||||
"Drang Armor": 0x154E0C28,
|
||||
"Drang Gauntlets": 0x154E1010,
|
||||
"Drang Shoes": 0x154E13F8,
|
||||
"Archdeacon White Crown": 0x13EF1480,
|
||||
"Archdeacon Holy Garb": 0x13EF1868,
|
||||
"Archdeacon Skirt": 0x13EF2038,
|
||||
"Antiquated Dress": 0x15D76068,
|
||||
"Antiquated Gloves": 0x15D76450,
|
||||
"Antiquated Skirt": 0x15D76838,
|
||||
"Ragged Mask": 0x148F4C20,
|
||||
"Crown of Dusk": 0x15D75C80,
|
||||
"Pharis's Hat": 0x1487AB00,
|
||||
"Old Sage's Blindfold": 0x11945BA0,
|
||||
|
||||
"Painting Guardian Hood": 0x156C8CC0,
|
||||
"Painting Guardian Gown": 0x156C90A8,
|
||||
"Painting Guardian Gloves": 0x156C9490,
|
||||
"Painting Guardian Waistcloth": 0x156C9878,
|
||||
"Brass Helm": 0x1501BD00,
|
||||
"Brass Armor": 0x1501C0E8,
|
||||
"Brass Gauntlets": 0x1501C4D0,
|
||||
"Brass Leggings": 0x1501C8B8,
|
||||
"Old Sorcerer Hat": 0x1496ED40,
|
||||
"Old Sorcerer Coat": 0x1496F128,
|
||||
"Old Sorcerer Gauntlets": 0x1496F510,
|
||||
"Old Sorcerer Boots": 0x1496F8F8,
|
||||
"Court Sorcerer Hood": 0x11BA8140,
|
||||
"Court Sorcerer Robe": 0x11BA8528,
|
||||
"Court Sorcerer Gloves": 0x11BA8910,
|
||||
"Court Sorcerer Trousers": 0x11BA8CF8,
|
||||
"Dragonslayer Helm": 0x158B1140,
|
||||
"Dragonslayer Armor": 0x158B1528,
|
||||
"Dragonslayer Gauntlets": 0x158B1910,
|
||||
"Dragonslayer Leggings": 0x158B1CF8,
|
||||
|
||||
"Hood of Prayer": 0x13AA6A60,
|
||||
"Robe of Prayer": 0x13AA6E48,
|
||||
"Skirt of Prayer": 0x13AA7618,
|
||||
"Winged Knight Helm": 0x12EBAE40,
|
||||
"Winged Knight Armor": 0x12EBB228,
|
||||
"Winged Knight Gauntlets": 0x12EBB610,
|
||||
"Winged Knight Leggings": 0x12EBB9F8,
|
||||
"Shadow Mask": 0x14D3F640,
|
||||
"Shadow Garb": 0x14D3FA28,
|
||||
"Shadow Gauntlets": 0x14D3FE10,
|
||||
"Shadow Leggings": 0x14D401F8,
|
||||
"Outrider Knight Helm": 0x1328B740,
|
||||
"Outrider Knight Armor": 0x1328BB28,
|
||||
"Outrider Knight Gauntlets": 0x1328BF10,
|
||||
"Outrider Knight Leggings": 0x1328C2F8,
|
||||
|
||||
"Cornyx's Wrap": 0x11946370,
|
||||
"Cornyx's Garb": 0x11945F88,
|
||||
"Cornyx's Skirt": 0x11946758
|
||||
}
|
||||
|
||||
dlc_armor_table = {
|
||||
"Slave Knight Hood": 0x134EDCE0,
|
||||
"Slave Knight Armor": 0x134EE0C8,
|
||||
"Slave Knight Gauntlets": 0x134EE4B0,
|
||||
"Slave Knight Leggings": 0x134EE898,
|
||||
"Vilhelm's Helm": 0x11312D00,
|
||||
"Vilhelm's Armor": 0x113130E8,
|
||||
"Vilhelm's Gauntlets": 0x113134D0,
|
||||
"Vilhelm's Leggings": 0x113138B8,
|
||||
#"Millwood Knight Helm": 0x139B2820, # SHOP ITEM
|
||||
#"Millwood Knight Armor": 0x139B2C08, # SHOP ITEM
|
||||
#"Millwood Knight Gauntlets": 0x139B2FF0, # SHOP ITEM
|
||||
#"Millwood Knight Leggings": 0x139B33D8, # SHOP ITEM
|
||||
|
||||
"Shira's Crown": 0x11C22260,
|
||||
"Shira's Armor": 0x11C22648,
|
||||
"Shira's Gloves": 0x11C22A30,
|
||||
"Shira's Trousers": 0x11C22E18,
|
||||
#"Lapp's Helm": 0x11E84800, # SHOP ITEM
|
||||
#"Lapp's Armor": 0x11E84BE8, # SHOP ITEM
|
||||
#"Lapp's Gauntlets": 0x11E84FD0, # SHOP ITEM
|
||||
#"Lapp's Leggings": 0x11E853B8, # SHOP ITEM
|
||||
#"Ringed Knight Hood": 0x13C8EEE0, # RANDOM ENEMY DROP
|
||||
#"Ringed Knight Armor": 0x13C8F2C8, # RANDOM ENEMY DROP
|
||||
#"Ringed Knight Gauntlets": 0x13C8F6B0, # RANDOM ENEMY DROP
|
||||
#"Ringed Knight Leggings": 0x13C8FA98, # RANDOM ENEMY DROP
|
||||
#"Harald Legion Armor": 0x13D83508, # RANDOM ENEMY DROP
|
||||
#"Harald Legion Gauntlets": 0x13D838F0, # RANDOM ENEMY DROP
|
||||
#"Harald Legion Leggings": 0x13D83CD8, # RANDOM ENEMY DROP
|
||||
"Iron Dragonslayer Helm": 0x1405F7E0,
|
||||
"Iron Dragonslayer Armor": 0x1405FBC8,
|
||||
"Iron Dragonslayer Gauntlets": 0x1405FFB0,
|
||||
"Iron Dragonslayer Leggings": 0x14060398,
|
||||
|
||||
"Ruin Sentinel Helm": 0x14CC5520,
|
||||
"Ruin Sentinel Armor": 0x14CC5908,
|
||||
"Ruin Sentinel Gauntlets": 0x14CC5CF0,
|
||||
"Ruin Sentinel Leggings": 0x14CC60D8,
|
||||
"Desert Pyromancer Hood": 0x14DB9760,
|
||||
"Desert Pyromancer Garb": 0x14DB9B48,
|
||||
"Desert Pyromancer Gloves": 0x14DB9F30,
|
||||
"Desert Pyromancer Skirt": 0x14DBA318,
|
||||
|
||||
#"Follower Helm": 0x137CA3A0, # RANDOM ENEMY DROP
|
||||
#"Follower Armor": 0x137CA788, # RANDOM ENEMY DROP
|
||||
#"Follower Gloves": 0x137CAB70, # RANDOM ENEMY DROP
|
||||
#"Follower Boots": 0x137CAF58, # RANDOM ENEMY DROP
|
||||
#"Ordained Hood": 0x135E1F20, # SHOP ITEM
|
||||
#"Ordained Dress": 0x135E2308, # SHOP ITEM
|
||||
#"Ordained Trousers": 0x135E2AD8, # SHOP ITEM
|
||||
"Black Witch Veil": 0x14FA1BE0,
|
||||
"Black Witch Hat": 0x14EAD9A0,
|
||||
"Black Witch Garb": 0x14EADD88,
|
||||
"Black Witch Wrappings": 0x14EAE170,
|
||||
"Black Witch Trousers": 0x14EAE558,
|
||||
"White Preacher Head": 0x14153A20,
|
||||
"Antiquated Plain Garb": 0x11B2E408
|
||||
}
|
||||
|
||||
rings_table = {
|
||||
"Estus Ring": 0x200050DC,
|
||||
"Covetous Silver Serpent Ring": 0x20004FB0,
|
||||
"Fire Clutch Ring": 0x2000501E,
|
||||
"Flame Stoneplate Ring": 0x20004E52,
|
||||
"Flynn's Ring": 0x2000503C,
|
||||
"Chloranthy Ring": 0x20004E2A,
|
||||
|
||||
"Morne's Ring": 0x20004F1A,
|
||||
"Sage Ring": 0x20004F38,
|
||||
"Aldrich's Sapphire": 0x20005096,
|
||||
"Lloyd's Sword Ring": 0x200050B4,
|
||||
"Poisonbite Ring": 0x20004E8E,
|
||||
"Deep Ring": 0x20004F60,
|
||||
"Lingering Dragoncrest Ring": 0x20004F2E,
|
||||
"Carthus Milkring": 0x20004FE2,
|
||||
"Witch's Ring": 0x20004F11,
|
||||
"Carthus Bloodring": 0x200050FA,
|
||||
|
||||
"Speckled Stoneplate Ring": 0x20004E7A,
|
||||
"Magic Clutch Ring": 0x2000500A,
|
||||
"Ring of the Sun's First Born": 0x20004F1B,
|
||||
"Pontiff's Right Eye": 0x2000510E, "Leo Ring": 0x20004EE8,
|
||||
"Dark Stoneplate Ring": 0x20004E70,
|
||||
"Reversal Ring": 0x20005104,
|
||||
"Ring of Favor": 0x20004E3E,
|
||||
"Bellowing Dragoncrest Ring": 0x20004F07,
|
||||
"Covetous Gold Serpent Ring": 0x20004FA6,
|
||||
"Dusk Crown Ring": 0x20004F4C,
|
||||
"Dark Clutch Ring": 0x20005028,
|
||||
"Cursebite Ring": 0x20004E98,
|
||||
"Sun Princess Ring": 0x20004FBA,
|
||||
"Aldrich's Ruby": 0x2000508C,
|
||||
"Scholar Ring": 0x20004EB6,
|
||||
"Fleshbite Ring": 0x20004EA2,
|
||||
"Hunter's Ring": 0x20004FF6,
|
||||
"Ashen Estus Ring": 0x200050E6,
|
||||
"Hornet Ring": 0x20004F9C,
|
||||
"Lightning Clutch Ring": 0x20005014,
|
||||
"Ring of Steel Protection": 0x20004E48,
|
||||
"Calamity Ring": 0x20005078,
|
||||
"Thunder Stoneplate Ring": 0x20004E5C,
|
||||
"Knight's Ring": 0x20004FEC,
|
||||
"Red Tearstone Ring": 0x20004ECA,
|
||||
"Dragonscale Ring": 0x2000515E,
|
||||
"Knight Slayer's Ring": 0x20005000,
|
||||
"Magic Stoneplate Ring": 0x20004E66,
|
||||
"Blue Tearstone Ring": 0x20004ED4 #given/dropped by Greirat
|
||||
}
|
||||
|
||||
dlc_ring_table = {
|
||||
"Havel's Ring": 0x20004E34,
|
||||
"Chillbite Ring": 0x20005208
|
||||
}
|
||||
|
||||
spells_table = {
|
||||
"Seek Guidance": 0x40360420,
|
||||
"Lightning Spear": 0x40362B30,
|
||||
"Atonement": 0x4039ADA0,
|
||||
"Great Magic Weapon": 0x40140118,
|
||||
"Iron Flesh": 0x40251430,
|
||||
"Lightning Stake": 0x40389C30,
|
||||
"Toxic Mist": 0x4024F108,
|
||||
"Sacred Flame": 0x40284880,
|
||||
"Dorhys' Gnawing": 0x40363EB8,
|
||||
"Great Heal": 0x40356FB0,
|
||||
"Lightning Blade": 0x4036C770,
|
||||
"Profaned Flame": 0x402575D8,
|
||||
"Wrath of the Gods": 0x4035E0F8,
|
||||
"Power Within": 0x40253B40,
|
||||
"Soul Stream": 0x4018B820,
|
||||
"Divine Pillars of Light": 0x4038C340,
|
||||
"Great Magic Barrier": 0x40365628,
|
||||
"Great Magic Shield": 0x40144F38,
|
||||
"Crystal Scroll": 0x40000856
|
||||
}
|
||||
|
||||
dlc_spells_table = {
|
||||
#"Boulder Heave": 0x40282170, # KILN STRAY DEMON
|
||||
#"Seething Chaos": 0x402896A0, # KILN DEMON PRINCES
|
||||
#"Old Moonlight": 0x4014FF00, # KILN MIDIR
|
||||
"Frozen Weapon": 0x401408E8,
|
||||
"Snap Freeze": 0x401A90C8,
|
||||
"Great Soul Dregs": 0x401879A0,
|
||||
"Flame Fan": 0x40258190,
|
||||
"Lightning Arrow": 0x40358B08,
|
||||
"Way of White Corona": 0x403642A0,
|
||||
"Projected Heal": 0x40364688,
|
||||
"Floating Chaos": 0x40257DA8
|
||||
}
|
||||
|
||||
misc_items_table = {
|
||||
"Tower Key": 0x400007DF,
|
||||
"Grave Key": 0x400007D9,
|
||||
"Cell Key": 0x400007DA,
|
||||
"Small Lothric Banner": 0x40000836,
|
||||
"Mortician's Ashes": 0x4000083B,
|
||||
"Braille Divine Tome of Carim": 0x40000847, # Shop
|
||||
"Great Swamp Pyromancy Tome": 0x4000084F, # Shop
|
||||
"Farron Coal ": 0x40000837, # Shop
|
||||
"Paladin's Ashes": 0x4000083D, # Shop
|
||||
"Deep Braille Divine Tome": 0x40000860, # Shop
|
||||
"Small Doll": 0x400007D5,
|
||||
"Golden Scroll": 0x4000085C,
|
||||
"Sage's Coal": 0x40000838, # Shop #Unique
|
||||
"Sage's Scroll": 0x40000854,
|
||||
"Dreamchaser's Ashes": 0x4000083C, # Shop #Unique
|
||||
"Cinders of a Lord - Abyss Watcher": 0x4000084B,
|
||||
"Cinders of a Lord - Yhorm the Giant": 0x4000084D,
|
||||
"Cinders of a Lord - Aldrich": 0x4000084C,
|
||||
"Grand Archives Key": 0x400007DE,
|
||||
"Basin of Vows": 0x40000845,
|
||||
"Cinders of a Lord - Lothric Prince": 0x4000084E,
|
||||
"Carthus Pyromancy Tome": 0x40000850,
|
||||
"Grave Warden's Ashes": 0x4000083E,
|
||||
"Grave Warden Pyromancy Tome": 0x40000853,
|
||||
"Quelana Pyromancy Tome": 0x40000852,
|
||||
"Izalith Pyromancy Tome": 0x40000851,
|
||||
"Greirat's Ashes": 0x4000083F,
|
||||
"Excrement-covered Ashes": 0x40000862,
|
||||
"Easterner's Ashes": 0x40000868,
|
||||
"Prisoner Chief's Ashes": 0x40000863,
|
||||
"Jailbreaker's Key": 0x400007D7,
|
||||
"Dragon Torso Stone": 0x4000017A,
|
||||
"Profaned Coal": 0x4000083A,
|
||||
"Xanthous Ashes": 0x40000864,
|
||||
"Old Cell Key": 0x400007DC,
|
||||
"Jailer's Key Ring": 0x400007D8,
|
||||
"Logan's Scroll": 0x40000855,
|
||||
"Storm Ruler": 0x006132D0,
|
||||
"Giant's Coal": 0x40000839,
|
||||
"Coiled Sword Fragment": 0x4000015F,
|
||||
"Dragon Chaser's Ashes": 0x40000867,
|
||||
"Twinkling Dragon Torso Stone": 0x40000184,
|
||||
"Braille Divine Tome of Lothric": 0x40000848,
|
||||
"Irina's Ashes": 0x40000843,
|
||||
"Karla's Ashes": 0x40000842,
|
||||
"Cornyx's Ashes": 0x40000841,
|
||||
"Orbeck's Ashes": 0x40000840
|
||||
}
|
||||
|
||||
dlc_misc_table = {
|
||||
"Captains Ashes": 0x4000086A,
|
||||
"Contraption Key": 0x4000086B, # Needed for Painted World
|
||||
"Small Envoy Banner": 0x4000086C # Needed to get to Ringed City from Dreg Heap
|
||||
}
|
||||
|
||||
key_items_list = {
|
||||
"Small Lothric Banner",
|
||||
"Basin of Vows",
|
||||
"Small Doll",
|
||||
"Storm Ruler",
|
||||
"Grand Archives Key",
|
||||
"Cinders of a Lord - Abyss Watcher",
|
||||
"Cinders of a Lord - Yhorm the Giant",
|
||||
"Cinders of a Lord - Aldrich",
|
||||
"Cinders of a Lord - Lothric Prince",
|
||||
"Mortician's Ashes",
|
||||
"Cell Key",
|
||||
"Tower Key",
|
||||
"Jailbreaker's Key",
|
||||
"Prisoner Chief's Ashes",
|
||||
"Old Cell Key",
|
||||
"Jailer's Key Ring",
|
||||
"Contraption Key",
|
||||
"Small Envoy Banner"
|
||||
}
|
||||
|
||||
item_tables = [weapons_upgrade_5_table, weapons_upgrade_10_table, shields_table,
|
||||
armor_table, rings_table, spells_table, misc_items_table, goods_table, goods_2_table, goods_3_table,
|
||||
dlc_weapons_upgrade_5_table, dlc_weapons_upgrade_10_table, dlc_shields_table, dlc_goods_table,
|
||||
dlc_armor_table, dlc_spells_table, dlc_ring_table, dlc_misc_table, dlc_goods_2_table]
|
||||
|
||||
item_dictionary = {**weapons_upgrade_5_table, **weapons_upgrade_10_table, **shields_table,
|
||||
**armor_table, **rings_table, **spells_table, **misc_items_table, **goods_table, **goods_2_table,
|
||||
**goods_3_table, **dlc_weapons_upgrade_5_table, **dlc_weapons_upgrade_10_table, **dlc_shields_table,
|
||||
**dlc_goods_table, **dlc_armor_table, **dlc_spells_table, **dlc_ring_table, **dlc_misc_table, **dlc_goods_2_table}
|
||||
|
||||
@@ -1,614 +0,0 @@
|
||||
"""
|
||||
Tools used to create this list :
|
||||
List of all items https://docs.google.com/spreadsheets/d/1nK2g7g6XJ-qphFAk1tjP3jZtlXWDQY-ItKLa_sniawo/edit#gid=1551945791
|
||||
Regular expression parser https://regex101.com/r/XdtiLR/2
|
||||
List of locations https://darksouls3.wiki.fextralife.com/Locations
|
||||
"""
|
||||
|
||||
fire_link_shrine_table = {
|
||||
# "FS: Coiled Sword": 0x40000859, You can still light the Firelink Shrine fire whether you have it or not, useless
|
||||
"FS: Broken Straight Sword": 0x001EF9B0,
|
||||
"FS: East-West Shield": 0x0142B930,
|
||||
"FS: Uchigatana": 0x004C4B40,
|
||||
"FS: Master's Attire": 0x148F5008,
|
||||
"FS: Master's Gloves": 0x148F53F0,
|
||||
}
|
||||
|
||||
firelink_shrine_bell_tower_table = {
|
||||
"FSBT: Covetous Silver Serpent Ring": 0x20004FB0,
|
||||
"FSBT: Fire Keeper Robe": 0x140D9CE8,
|
||||
"FSBT: Fire Keeper Gloves": 0x140DA0D0,
|
||||
"FSBT: Fire Keeper Skirt": 0x140DA4B8,
|
||||
"FSBT: Estus Ring": 0x200050DC,
|
||||
"FSBT: Fire Keeper Soul": 0x40000186
|
||||
}
|
||||
|
||||
high_wall_of_lothric = {
|
||||
"HWL: Deep Battle Axe": 0x0006AFA54,
|
||||
"HWL: Club": 0x007A1200,
|
||||
"HWL: Claymore": 0x005BDBA0,
|
||||
"HWL: Binoculars": 0x40000173,
|
||||
"HWL: Longbow": 0x00D689E0,
|
||||
"HWL: Mail Breaker": 0x002DEDD0,
|
||||
"HWL: Broadsword": 0x001ED2A0,
|
||||
"HWL: Silver Eagle Kite Shield": 0x014418C0,
|
||||
"HWL: Astora's Straight Sword": 0x002191C0,
|
||||
"HWL: Cell Key": 0x400007DA,
|
||||
"HWL: Rapier": 0x002E14E0,
|
||||
"HWL: Lucerne": 0x0098BD90,
|
||||
"HWL: Small Lothric Banner": 0x40000836,
|
||||
"HWL: Basin of Vows": 0x40000845,
|
||||
"HWL: Soul of Boreal Valley Vordt": 0x400002CF,
|
||||
"HWL: Soul of the Dancer": 0x400002CA,
|
||||
"HWL: Way of Blue Covenant": 0x2000274C,
|
||||
"HWL: Greirat's Ashes": 0x4000083F,
|
||||
"HWL: Blue Tearstone Ring": 0x20004ED4 #given/dropped by Greirat
|
||||
}
|
||||
|
||||
undead_settlement_table = {
|
||||
"US: Small Leather Shield": 0x01315410,
|
||||
"US: Whip": 0x00B71B00,
|
||||
"US: Reinforced Club": 0x007A8730,
|
||||
"US: Blue Wooden Shield": 0x0143F1B0,
|
||||
|
||||
"US: Cleric Hat": 0x11D905C0,
|
||||
"US: Cleric Blue Robe": 0x11D909A8,
|
||||
"US: Cleric Gloves": 0x11D90D90,
|
||||
"US: Cleric Trousers": 0x11D91178,
|
||||
|
||||
"US: Mortician's Ashes": 0x4000083B,
|
||||
"US: Caestus": 0x00A7FFD0,
|
||||
"US: Plank Shield": 0x01346150,
|
||||
"US: Flame Stoneplate Ring": 0x20004E52,
|
||||
"US: Caduceus Round Shield": 0x01341330,
|
||||
"US: Fire Clutch Ring": 0x2000501E,
|
||||
"US: Partizan": 0x0089C970,
|
||||
"US: Bloodbite Ring": 0x20004E84,
|
||||
|
||||
"US: Red Hilted Halberd": 0x009AB960,
|
||||
"US: Saint's Talisman": 0x00CACA10,
|
||||
"US: Irithyll Straight Sword": 0x0020A760,
|
||||
"US: Large Club": 0x007AFC60,
|
||||
"US: Northern Helm": 0x116E3600,
|
||||
"US: Northern Armor": 0x116E39E8,
|
||||
"US: Northern Gloves": 0x116E3DD0,
|
||||
"US: Northern Trousers": 0x116E41B8,
|
||||
"US: Flynn's Ring": 0x2000503C,
|
||||
|
||||
"US: Mirrah Vest": 0x15204568,
|
||||
"US: Mirrah Gloves": 0x15204950,
|
||||
"US: Mirrah Trousers": 0x15204D38,
|
||||
|
||||
"US: Chloranthy Ring": 0x20004E2A,
|
||||
"US: Loincloth": 0x148F57D8,
|
||||
"US: Wargod Wooden Shield": 0x0144DC10,
|
||||
|
||||
"US: Loretta's Bone": 0x40000846,
|
||||
|
||||
"US: Hand Axe": 0x006ACFC0,
|
||||
"US: Great Scythe": 0x00989680,
|
||||
"US: Soul of the Rotted Greatwood": 0x400002D7,
|
||||
"US: Hawk Ring": 0x20004F92,
|
||||
"US: Warrior of Sunlight Covenant": 0x20002738,
|
||||
"US: Blessed Red and White Shield": 0x01343FB9,
|
||||
"US: Irina's Ashes": 0x40000843,
|
||||
"US: Cornyx's Ashes": 0x40000841,
|
||||
"US: Cornyx's Wrap": 0x11946370,
|
||||
"US: Cornyx's Garb": 0x11945F88,
|
||||
"US: Cornyx's Skirt": 0x11946758,
|
||||
"US: Pyromancy Flame": 0x00CC77C0 #given/dropped by Cornyx
|
||||
}
|
||||
|
||||
road_of_sacrifice_table = {
|
||||
"RS: Brigand Twindaggers": 0x00F50E60,
|
||||
|
||||
"RS: Brigand Hood": 0x148009E0,
|
||||
"RS: Brigand Armor": 0x14800DC8,
|
||||
"RS: Brigand Gauntlets": 0x148011B0,
|
||||
"RS: Brigand Trousers": 0x14801598,
|
||||
|
||||
"RS: Butcher Knife": 0x006BE130,
|
||||
"RS: Brigand Axe": 0x006B1DE0,
|
||||
"RS: Braille Divine Tome of Carim": 0x40000847,
|
||||
"RS: Morne's Ring": 0x20004F1A,
|
||||
"RS: Twin Dragon Greatshield": 0x01513820,
|
||||
"RS: Heretic's Staff": 0x00C8F550,
|
||||
|
||||
"RS: Sorcerer Hood": 0x11C9C380,
|
||||
"RS: Sorcerer Robe": 0x11C9C768,
|
||||
"RS: Sorcerer Gloves": 0x11C9CB50,
|
||||
"RS: Sorcerer Trousers": 0x11C9CF38,
|
||||
|
||||
"RS: Sage Ring": 0x20004F38,
|
||||
|
||||
"RS: Fallen Knight Helm": 0x1121EAC0,
|
||||
"RS: Fallen Knight Armor": 0x1121EEA8,
|
||||
"RS: Fallen Knight Gauntlets": 0x1121F290,
|
||||
"RS: Fallen Knight Trousers": 0x1121F678,
|
||||
|
||||
"RS: Conjurator Hood": 0x149E8E60,
|
||||
"RS: Conjurator Robe": 0x149E9248,
|
||||
"RS: Conjurator Manchettes": 0x149E9630,
|
||||
"RS: Conjurator Boots": 0x149E9A18,
|
||||
|
||||
"RS: Great Swamp Pyromancy Tome": 0x4000084F,
|
||||
|
||||
"RS: Great Club": 0x007B4A80,
|
||||
"RS: Exile Greatsword": 0x005DD770,
|
||||
|
||||
"RS: Farron Coal ": 0x40000837,
|
||||
|
||||
"RS: Sellsword Twinblades": 0x00F42400,
|
||||
"RS: Sellsword Helm": 0x11481060,
|
||||
"RS: Sellsword Armor": 0x11481448,
|
||||
"RS: Sellsword Gauntlet": 0x11481830,
|
||||
"RS: Sellsword Trousers": 0x11481C18,
|
||||
|
||||
"RS: Golden Falcon Shield": 0x01354BB0,
|
||||
|
||||
"RS: Herald Helm": 0x114FB180,
|
||||
"RS: Herald Armor": 0x114FB568,
|
||||
"RS: Herald Gloves": 0x114FB950,
|
||||
"RS: Herald Trousers": 0x114FBD38,
|
||||
|
||||
"RS: Grass Crest Shield": 0x01437C80,
|
||||
"RS: Soul of a Crystal Sage": 0x400002CB,
|
||||
"RS: Great Swamp Ring": 0x20004F10,
|
||||
"RS: Orbeck's Ashes": 0x40000840
|
||||
}
|
||||
|
||||
cathedral_of_the_deep_table = {
|
||||
"CD: Paladin's Ashes": 0x4000083D,
|
||||
"CD: Spider Shield": 0x01435570,
|
||||
"CD: Crest Shield": 0x01430750,
|
||||
"CD: Notched Whip": 0x00B7DE50,
|
||||
"CD: Astora Greatsword": 0x005C9EF0,
|
||||
"CD: Executioner's Greatsword": 0x0021DFE0,
|
||||
"CD: Curse Ward Greatshield": 0x01518640,
|
||||
"CD: Saint-tree Bellvine": 0x00C9DFB0,
|
||||
"CD: Poisonbite Ring": 0x20004E8E,
|
||||
|
||||
"CD: Lloyd's Sword Ring": 0x200050B4,
|
||||
"CD: Seek Guidance": 0x40360420,
|
||||
|
||||
"CD: Aldrich's Sapphire": 0x20005096,
|
||||
"CD: Deep Braille Divine Tome": 0x40000860,
|
||||
|
||||
"CD: Saint Bident": 0x008C1360,
|
||||
"CD: Maiden Hood": 0x14BD12E0,
|
||||
"CD: Maiden Robe": 0x14BD16C8,
|
||||
"CD: Maiden Gloves": 0x14BD1AB0,
|
||||
"CD: Maiden Skirt": 0x14BD1E98,
|
||||
"CD: Drang Armor": 0x154E0C28,
|
||||
"CD: Drang Gauntlets": 0x154E1010,
|
||||
"CD: Drang Shoes": 0x154E13F8,
|
||||
"CD: Drang Hammers": 0x00F61FD0,
|
||||
"CD: Deep Ring": 0x20004F60,
|
||||
|
||||
"CD: Archdeacon White Crown": 0x13EF1480,
|
||||
"CD: Archdeacon Holy Garb": 0x13EF1868,
|
||||
"CD: Archdeacon Skirt": 0x13EF2038,
|
||||
|
||||
"CD: Arbalest": 0x00D662D0,
|
||||
"CD: Small Doll": 0x400007D5,
|
||||
"CD: Soul of the Deacons of the Deep": 0x400002D9,
|
||||
"CD: Rosaria's Fingers Covenant": 0x20002760,
|
||||
}
|
||||
|
||||
farron_keep_table = {
|
||||
"FK: Ragged Mask": 0x148F4C20,
|
||||
"FK: Iron Flesh": 0x40251430,
|
||||
"FK: Golden Scroll": 0x4000085C,
|
||||
|
||||
"FK: Antiquated Dress": 0x15D76068,
|
||||
"FK: Antiquated Gloves": 0x15D76450,
|
||||
"FK: Antiquated Skirt": 0x15D76838,
|
||||
|
||||
"FK: Nameless Knight Helm": 0x143B5FC0,
|
||||
"FK: Nameless Knight Armor": 0x143B63A8,
|
||||
"FK: Nameless Knight Gauntlets": 0x143B6790,
|
||||
"FK: Nameless Knight Leggings": 0x143B6B78,
|
||||
|
||||
"FK: Sunlight Talisman": 0x00CA54E0,
|
||||
"FK: Wolf's Blood Swordgrass": 0x4000016E,
|
||||
"FK: Greatsword": 0x005C50D0,
|
||||
|
||||
"FK: Sage's Coal": 0x40000838,
|
||||
"FK: Stone Parma": 0x01443FD0,
|
||||
"FK: Sage's Scroll": 0x40000854,
|
||||
"FK: Crown of Dusk": 0x15D75C80,
|
||||
|
||||
"FK: Lingering Dragoncrest Ring": 0x20004F2E,
|
||||
"FK: Pharis's Hat": 0x1487AB00,
|
||||
"FK: Black Bow of Pharis": 0x00D7E970,
|
||||
|
||||
"FK: Dreamchaser's Ashes": 0x4000083C,
|
||||
"FK: Great Axe": 0x006B9310,
|
||||
"FK: Dragon Crest Shield": 0x01432E60,
|
||||
"FK: Lightning Spear": 0x40362B30,
|
||||
"FK: Atonement": 0x4039ADA0,
|
||||
"FK: Great Magic Weapon": 0x40140118,
|
||||
"FK: Cinders of a Lord - Abyss Watcher": 0x4000084B,
|
||||
"FK: Soul of the Blood of the Wolf": 0x400002CD,
|
||||
"FK: Soul of a Stray Demon": 0x400002E7,
|
||||
"FK: Watchdogs of Farron Covenant": 0x20002724,
|
||||
}
|
||||
|
||||
catacombs_of_carthus_table = {
|
||||
"CC: Carthus Pyromancy Tome": 0x40000850,
|
||||
"CC: Carthus Milkring": 0x20004FE2,
|
||||
"CC: Grave Warden's Ashes": 0x4000083E,
|
||||
"CC: Carthus Bloodring": 0x200050FA,
|
||||
"CC: Grave Warden Pyromancy Tome": 0x40000853,
|
||||
"CC: Old Sage's Blindfold": 0x11945BA0,
|
||||
"CC: Witch's Ring": 0x20004F11,
|
||||
"CC: Black Blade": 0x004CC070,
|
||||
"CC: Soul of High Lord Wolnir": 0x400002D6,
|
||||
"CC: Soul of a Demon": 0x400002E3,
|
||||
}
|
||||
|
||||
smouldering_lake_table = {
|
||||
"SL: Shield of Want": 0x0144B500,
|
||||
"SL: Speckled Stoneplate Ring": 0x20004E7A,
|
||||
"SL: Dragonrider Bow": 0x00D6B0F0,
|
||||
"SL: Lightning Stake": 0x40389C30,
|
||||
"SL: Izalith Pyromancy Tome": 0x40000851,
|
||||
"SL: Black Knight Sword": 0x005F5E10,
|
||||
"SL: Quelana Pyromancy Tome": 0x40000852,
|
||||
"SL: Toxic Mist": 0x4024F108,
|
||||
"SL: White Hair Talisman": 0x00CAF120,
|
||||
"SL: Izalith Staff": 0x00C96A80,
|
||||
"SL: Sacred Flame": 0x40284880,
|
||||
"SL: Fume Ultra Greatsword": 0x0060E4B0,
|
||||
"SL: Black Iron Greatshield": 0x0150EA00,
|
||||
"SL: Soul of the Old Demon King": 0x400002D0,
|
||||
"SL: Knight Slayer's Ring": 0x20005000,
|
||||
}
|
||||
|
||||
irithyll_of_the_boreal_valley_table = {
|
||||
"IBV: Dorhys' Gnawing": 0x40363EB8,
|
||||
"IBV: Witchtree Branch": 0x00C94370,
|
||||
"IBV: Magic Clutch Ring": 0x2000500A,
|
||||
"IBV: Ring of the Sun's First Born": 0x20004F1B,
|
||||
"IBV: Roster of Knights": 0x4000006C,
|
||||
"IBV: Pontiff's Right Eye": 0x2000510E,
|
||||
|
||||
"IBV: Yorshka's Spear": 0x008C3A70,
|
||||
"IBV: Great Heal": 0x40356FB0,
|
||||
|
||||
"IBV: Smough's Great Hammer": 0x007E30B0,
|
||||
"IBV: Leo Ring": 0x20004EE8,
|
||||
"IBV: Excrement-covered Ashes": 0x40000862,
|
||||
|
||||
"IBV: Dark Stoneplate Ring": 0x20004E70,
|
||||
"IBV: Easterner's Ashes": 0x40000868,
|
||||
"IBV: Painting Guardian's Curved Sword": 0x003E6890,
|
||||
"IBV: Painting Guardian Hood": 0x156C8CC0,
|
||||
"IBV: Painting Guardian Gown": 0x156C90A8,
|
||||
"IBV: Painting Guardian Gloves": 0x156C9490,
|
||||
"IBV: Painting Guardian Waistcloth": 0x156C9878,
|
||||
"IBV: Dragonslayer Greatbow": 0x00CF8500,
|
||||
"IBV: Reversal Ring": 0x20005104,
|
||||
"IBV: Brass Helm": 0x1501BD00,
|
||||
"IBV: Brass Armor": 0x1501C0E8,
|
||||
"IBV: Brass Gauntlets": 0x1501C4D0,
|
||||
"IBV: Brass Leggings": 0x1501C8B8,
|
||||
"IBV: Ring of Favor": 0x20004E3E,
|
||||
"IBV: Golden Ritual Spear": 0x00C83200,
|
||||
"IBV: Soul of Pontiff Sulyvahn": 0x400002D4,
|
||||
"IBV: Aldrich Faithful Covenant": 0x2000272E,
|
||||
"IBV: Drang Twinspears": 0x00F5AAA0,
|
||||
}
|
||||
|
||||
irithyll_dungeon_table = {
|
||||
"ID: Bellowing Dragoncrest Ring": 0x20004F07,
|
||||
"ID: Jailbreaker's Key": 0x400007D7,
|
||||
"ID: Prisoner Chief's Ashes": 0x40000863,
|
||||
"ID: Old Sorcerer Hat": 0x1496ED40,
|
||||
"ID: Old Sorcerer Coat": 0x1496F128,
|
||||
"ID: Old Sorcerer Gauntlets": 0x1496F510,
|
||||
"ID: Old Sorcerer Boots": 0x1496F8F8,
|
||||
"ID: Great Magic Shield": 0x40144F38,
|
||||
|
||||
"ID: Dragon Torso Stone": 0x4000017A,
|
||||
"ID: Lightning Blade": 0x4036C770,
|
||||
"ID: Profaned Coal": 0x4000083A,
|
||||
"ID: Xanthous Ashes": 0x40000864,
|
||||
"ID: Old Cell Key": 0x400007DC,
|
||||
"ID: Pickaxe": 0x007DE290,
|
||||
"ID: Profaned Flame": 0x402575D8,
|
||||
"ID: Covetous Gold Serpent Ring": 0x20004FA6,
|
||||
"ID: Jailer's Key Ring": 0x400007D8,
|
||||
"ID: Dusk Crown Ring": 0x20004F4C,
|
||||
"ID: Dark Clutch Ring": 0x20005028,
|
||||
"ID: Karla's Ashes": 0x40000842
|
||||
}
|
||||
|
||||
profaned_capital_table = {
|
||||
"PC: Cursebite Ring": 0x20004E98,
|
||||
"PC: Court Sorcerer Hood": 0x11BA8140,
|
||||
"PC: Court Sorcerer Robe": 0x11BA8528,
|
||||
"PC: Court Sorcerer Gloves": 0x11BA8910,
|
||||
"PC: Court Sorcerer Trousers": 0x11BA8CF8,
|
||||
"PC: Wrath of the Gods": 0x4035E0F8,
|
||||
"PC: Logan's Scroll": 0x40000855,
|
||||
"PC: Eleonora": 0x006CCB90,
|
||||
"PC: Court Sorcerer's Staff": 0x00C91C60,
|
||||
"PC: Greatshield of Glory": 0x01515F30,
|
||||
"PC: Storm Ruler": 0x006132D0,
|
||||
"PC: Cinders of a Lord - Yhorm the Giant": 0x4000084D,
|
||||
"PC: Soul of Yhorm the Giant": 0x400002DC,
|
||||
}
|
||||
|
||||
anor_londo_table = {
|
||||
"AL: Giant's Coal": 0x40000839,
|
||||
"AL: Sun Princess Ring": 0x20004FBA,
|
||||
"AL: Aldrich's Ruby": 0x2000508C,
|
||||
"AL: Cinders of a Lord - Aldrich": 0x4000084C,
|
||||
"AL: Soul of Aldrich": 0x400002D5,
|
||||
}
|
||||
|
||||
lothric_castle_table = {
|
||||
"LC: Hood of Prayer": 0x13AA6A60,
|
||||
"LC: Robe of Prayer": 0x13AA6E48,
|
||||
"LC: Skirt of Prayer": 0x13AA7618,
|
||||
|
||||
"LC: Sacred Bloom Shield": 0x013572C0,
|
||||
"LC: Winged Knight Helm": 0x12EBAE40,
|
||||
"LC: Winged Knight Armor": 0x12EBB228,
|
||||
"LC: Winged Knight Gauntlets": 0x12EBB610,
|
||||
"LC: Winged Knight Leggings": 0x12EBB9F8,
|
||||
|
||||
"LC: Greatlance": 0x008A8CC0,
|
||||
"LC: Sniper Crossbow": 0x00D83790,
|
||||
"LC: Spirit Tree Crest Shield": 0x014466E0,
|
||||
"LC: Red Tearstone Ring": 0x20004ECA,
|
||||
"LC: Caitha's Chime": 0x00CA06C0,
|
||||
"LC: Braille Divine Tome of Lothric": 0x40000848,
|
||||
"LC: Knight's Ring": 0x20004FEC,
|
||||
"LC: Irithyll Rapier": 0x002E8A10,
|
||||
"LC: Sunlight Straight Sword": 0x00203230,
|
||||
"LC: Soul of Dragonslayer Armour": 0x400002D1,
|
||||
|
||||
# The Black Hand Gotthard corpse appears when you have defeated Yhorm and Aldrich and triggered the cutscene
|
||||
"LC: Grand Archives Key": 0x400007DE, # On Black Hand Gotthard corpse
|
||||
"LC: Gotthard Twinswords": 0x00F53570 # On Black Hand Gotthard corpse
|
||||
}
|
||||
|
||||
consumed_king_garden_table = {
|
||||
"CKG: Dragonscale Ring": 0x2000515E,
|
||||
"CKG: Shadow Mask": 0x14D3F640,
|
||||
"CKG: Shadow Garb": 0x14D3FA28,
|
||||
"CKG: Shadow Gauntlets": 0x14D3FE10,
|
||||
"CKG: Shadow Leggings": 0x14D401F8,
|
||||
"CKG: Claw": 0x00A7D8C0,
|
||||
"CKG: Soul of Consumed Oceiros": 0x400002CE,
|
||||
"CKG: Magic Stoneplate Ring": 0x20004E66,
|
||||
# "CKG: Path of the Dragon Gesture": 0x40002346, I can't technically randomize it as it is a gesture and not an item
|
||||
}
|
||||
|
||||
grand_archives_table = {
|
||||
"GA: Avelyn": 0x00D6FF10,
|
||||
"GA: Witch's Locks": 0x00B7B740,
|
||||
"GA: Power Within": 0x40253B40,
|
||||
"GA: Scholar Ring": 0x20004EB6,
|
||||
"GA: Soul Stream": 0x4018B820,
|
||||
"GA: Fleshbite Ring": 0x20004EA2,
|
||||
"GA: Crystal Chime": 0x00CA2DD0,
|
||||
"GA: Golden Wing Crest Shield": 0x0143CAA0,
|
||||
"GA: Onikiri and Ubadachi": 0x00F58390,
|
||||
"GA: Hunter's Ring": 0x20004FF6,
|
||||
"GA: Divine Pillars of Light": 0x4038C340,
|
||||
"GA: Cinders of a Lord - Lothric Prince": 0x4000084E,
|
||||
"GA: Soul of the Twin Princes": 0x400002DB,
|
||||
"GA: Sage's Crystal Staff": 0x00C8CE40,
|
||||
"GA: Outrider Knight Helm": 0x1328B740,
|
||||
"GA: Outrider Knight Armor": 0x1328BB28,
|
||||
"GA: Outrider Knight Gauntlets": 0x1328BF10,
|
||||
"GA: Outrider Knight Leggings": 0x1328C2F8,
|
||||
"GA: Crystal Scroll": 0x40000856,
|
||||
}
|
||||
|
||||
untended_graves_table = {
|
||||
"UG: Ashen Estus Ring": 0x200050E6,
|
||||
"UG: Black Knight Glaive": 0x009AE070,
|
||||
"UG: Hornet Ring": 0x20004F9C,
|
||||
"UG: Chaos Blade": 0x004C9960,
|
||||
"UG: Blacksmith Hammer": 0x007E57C0,
|
||||
"UG: Eyes of a Fire Keeper": 0x4000085A,
|
||||
"UG: Coiled Sword Fragment": 0x4000015F,
|
||||
"UG: Soul of Champion Gundyr": 0x400002C8,
|
||||
}
|
||||
|
||||
archdragon_peak_table = {
|
||||
"AP: Lightning Clutch Ring": 0x20005014,
|
||||
"AP: Ancient Dragon Greatshield": 0x013599D0,
|
||||
"AP: Ring of Steel Protection": 0x20004E48,
|
||||
"AP: Calamity Ring": 0x20005078,
|
||||
"AP: Drakeblood Greatsword": 0x00609690,
|
||||
"AP: Dragonslayer Spear": 0x008CAFA0,
|
||||
|
||||
"AP: Thunder Stoneplate Ring": 0x20004E5C,
|
||||
"AP: Great Magic Barrier": 0x40365628,
|
||||
"AP: Dragon Chaser's Ashes": 0x40000867,
|
||||
"AP: Twinkling Dragon Torso Stone": 0x40000184,
|
||||
"AP: Dragonslayer Helm": 0x158B1140,
|
||||
"AP: Dragonslayer Armor": 0x158B1528,
|
||||
"AP: Dragonslayer Gauntlets": 0x158B1910,
|
||||
"AP: Dragonslayer Leggings": 0x158B1CF8,
|
||||
"AP: Ricard's Rapier": 0x002E3BF0,
|
||||
"AP: Soul of the Nameless King": 0x400002D2,
|
||||
"AP: Dragon Tooth": 0x007E09A0,
|
||||
"AP: Havel's Greatshield": 0x013376F0,
|
||||
}
|
||||
|
||||
painted_world_table = { # DLC
|
||||
"PW: Follower Javelin": 0x008CD6B0,
|
||||
"PW: Frozen Weapon": 0x401408E8,
|
||||
"PW: Millwood Greatbow": 0x00D85EA0,
|
||||
"PW: Captains Ashes": 0x4000086A,
|
||||
"PW: Millwood Battle Axe": 0x006D67D0,
|
||||
"PW: Ethereal Oak Shield": 0x01450320,
|
||||
"PW: Crow Quills": 0x00F66DF0,
|
||||
"PW: Slave Knight Hood": 0x134EDCE0,
|
||||
"PW: Slave Knight Armor": 0x134EE0C8,
|
||||
"PW: Slave Knight Gauntlets": 0x134EE4B0,
|
||||
"PW: Slave Knight Leggings": 0x134EE898,
|
||||
"PW: Way of White Corona": 0x403642A0,
|
||||
"PW: Crow Talons": 0x00A89C10,
|
||||
"PW: Quakestone Hammer": 0x007ECCF0,
|
||||
"PW: Earth Seeker": 0x006D8EE0,
|
||||
"PW: Follower Torch": 0x015F1AD0,
|
||||
"PW: Follower Shield": 0x0135C0E0,
|
||||
"PW: Follower Sabre": 0x003EDDC0,
|
||||
"PW: Snap Freeze": 0x401A90C8,
|
||||
"PW: Floating Chaos": 0x40257DA8,
|
||||
"PW: Pyromancer's Parting Flame": 0x00CC9ED0,
|
||||
"PW: Vilhelm's Helm": 0x11312D00,
|
||||
"PW: Vilhelm's Armor": 0x113130E8,
|
||||
"PW: Vilhelm's Gauntlets": 0x113134D0,
|
||||
"PW: Vilhelm's Leggings": 0x113138B8,
|
||||
"PW: Valorheart": 0x00F646E0, # GRAVETENDER FIGHT
|
||||
"PW: Champions Bones": 0x40000869, # GRAVETENDER FIGHT
|
||||
"PW: Onyx Blade": 0x00222E00, # VILHELM FIGHT
|
||||
"PW: Soul of Sister Friede": 0x400002E8,
|
||||
"PW: Titanite Slab": 0x400003EB,
|
||||
"PW: Chillbite Ring": 0x20005208,
|
||||
"PW: Contraption Key": 0x4000086B # VILHELM FIGHT/NEEDED TO PROGRESS THROUGH PW
|
||||
}
|
||||
|
||||
dreg_heap_table = { # DLC
|
||||
"DH: Loincloth": 0x11B2EBD8,
|
||||
"DH: Aquamarine Dagger": 0x00116520,
|
||||
"DH: Murky Hand Scythe": 0x00118C30,
|
||||
"DH: Murky Longstaff": 0x00CCC5E0,
|
||||
"DH: Great Soul Dregs": 0x401879A0,
|
||||
"DH: Lothric War Banner": 0x00CCC5E0,
|
||||
"DH: Projected Heal": 0x40364688,
|
||||
"DH: Desert Pyromancer Hood": 0x14DB9760,
|
||||
"DH: Desert Pyromancer Garb": 0x14DB9B48,
|
||||
"DH: Desert Pyromancer Gloves": 0x14DB9F30,
|
||||
"DH: Desert Pyromancer Skirt": 0x14DBA318,
|
||||
"DH: Giant Door Shield": 0x00F5F8C0,
|
||||
"DH: Herald Curved Greatsword": 0x006159E0,
|
||||
"DH: Flame Fan": 0x40258190,
|
||||
"DH: Soul of the Demon Prince": 0x400002EA,
|
||||
"DH: Small Envoy Banner": 0x4000086C # NEEDED TO TRAVEL TO RINGED CITY
|
||||
}
|
||||
|
||||
ringed_city_table = { # DLC
|
||||
"RC: Ruin Sentinel Helm": 0x14CC5520,
|
||||
"RC: Ruin Sentinel Armor": 0x14CC5908,
|
||||
"RC: Ruin Sentinel Gauntlets": 0x14CC5CF0,
|
||||
"RC: Ruin Sentinel Leggings": 0x14CC60D8,
|
||||
"RC: Black Witch Veil": 0x14FA1BE0,
|
||||
"RC: Black Witch Hat": 0x14EAD9A0,
|
||||
"RC: Black Witch Garb": 0x14EADD88,
|
||||
"RC: Black Witch Wrappings": 0x14EAE170,
|
||||
"RC: Black Witch Trousers": 0x14EAE558,
|
||||
"RC: White Preacher Head": 0x14153A20,
|
||||
"RC: Havel's Ring": 0x20004E34,
|
||||
"RC: Ringed Knight Spear": 0x008CFDC0,
|
||||
"RC: Dragonhead Shield": 0x0135E7F0,
|
||||
"RC: Ringed Knight Straight Sword": 0x00225510,
|
||||
"RC: Preacher's Right Arm": 0x00CD1400,
|
||||
"RC: White Birch Bow": 0x00D77440,
|
||||
"RC: Church Guardian Shiv": 0x4000013B, # Assigned to "Demon's Scar"
|
||||
"RC: Dragonhead Greatshield": 0x01452A30,
|
||||
"RC: Ringed Knight Paired Greatswords": 0x00F69500,
|
||||
"RC: Shira's Crown": 0x11C22260,
|
||||
"RC: Shira's Armor": 0x11C22648,
|
||||
"RC: Shira's Gloves": 0x11C22A30,
|
||||
"RC: Shira's Trousers": 0x11C22E18,
|
||||
"RC: Titanite Slab": 0x400003EB, # SHIRA DROP
|
||||
"RC: Crucifix of the Mad King": 0x008D4BE0, # SHIRA DROP
|
||||
"RC: Sacred Chime of Filianore": 0x00CCECF0, # SHIRA DROP
|
||||
"RC: Iron Dragonslayer Helm": 0x1405F7E0,
|
||||
"RC: Iron Dragonslayer Armor": 0x1405FBC8,
|
||||
"RC: Iron Dragonslayer Gauntlets": 0x1405FFB0,
|
||||
"RC: Iron Dragonslayer Leggings": 0x14060398,
|
||||
"RC: Lightning Arrow": 0x40358B08,
|
||||
"RC: Ritual Spear Fragment": 0x4000028A, # Assigned to "Frayed Blade"
|
||||
"RC: Antiquated Plain Garb": 0x11B2E408,
|
||||
"RC: Violet Wrappings": 0x11B2E7F0, # Assigned to "Gael's Greatsword"
|
||||
"RC: Soul of Darkeater Midir": 0x400002EB,
|
||||
"RC: Soul of Slave Knight Gael": 0x400002E9,
|
||||
"RC: Blood of the Dark Souls": 0x4000086E, # Assigned to "Repeating Crossbow"
|
||||
}
|
||||
|
||||
progressive_locations = {
|
||||
# Upgrade materials
|
||||
**{"Titanite Shard #"+str(i): 0x400003E8 for i in range(1, 11)},
|
||||
**{"Large Titanite Shard #"+str(i): 0x400003E9 for i in range(1, 11)},
|
||||
**{"Titanite Chunk #"+str(i): 0x400003EA for i in range(1, 6)},
|
||||
**{"Titanite Slab #"+str(i): 0x400003EB for i in range(1, 4)},
|
||||
|
||||
# Healing
|
||||
**{"Estus Shard #"+str(i): 0x4000085D for i in range(1, 16)},
|
||||
**{"Undead Bone Shard #"+str(i): 0x4000085F for i in range(1, 6)},
|
||||
|
||||
# Items
|
||||
**{"Firebomb #"+str(i): 0x40000124 for i in range(1, 5)},
|
||||
**{"Throwing Knife #"+str(i): 0x40000136 for i in range(1, 3)},
|
||||
|
||||
# Souls
|
||||
**{"Soul of a Deserted Corpse #" + str(i): 0x40000191 for i in range(1, 6)},
|
||||
**{"Large Soul of a Deserted Corpse #" + str(i): 0x40000192 for i in range(1, 6)},
|
||||
**{"Soul of an Unknown Traveler #" + str(i): 0x40000193 for i in range(1, 6)},
|
||||
**{"Large Soul of an Unknown Traveler #" + str(i): 0x40000194 for i in range(1, 6)}
|
||||
}
|
||||
|
||||
progressive_locations_2 = {
|
||||
##Added by Br00ty
|
||||
"HWL: Gold Pine Resin #": 0x4000014B,
|
||||
"US: Charcoal Pine Resin #": 0x4000014A,
|
||||
"FK: Gold Pine Bundle #": 0x40000155,
|
||||
"CC: Carthus Rouge #": 0x4000014F,
|
||||
"ID: Pale Pine Resin #": 0x40000150,
|
||||
**{"Titanite Scale #" + str(i): 0x400003FC for i in range(1, 27)},
|
||||
**{"Fading Soul #" + str(i): 0x40000190 for i in range(1, 4)},
|
||||
**{"Ring of Sacrifice #"+str(i): 0x20004EF2 for i in range(1, 5)},
|
||||
**{"Homeward Bone #"+str(i): 0x4000015E for i in range(1, 17)},
|
||||
**{"Ember #"+str(i): 0x400001F4 for i in range(1, 46)},
|
||||
}
|
||||
|
||||
progressive_locations_3 = {
|
||||
**{"Green Blossom #" + str(i): 0x40000104 for i in range(1, 7)},
|
||||
**{"Human Pine Resin #" + str(i): 0x4000014E for i in range(1, 3)},
|
||||
**{"Charcoal Pine Bundle #" + str(i): 0x40000154 for i in range(1, 3)},
|
||||
**{"Rotten Pine Resin #" + str(i): 0x40000157 for i in range(1, 3)},
|
||||
**{"Pale Tongue #" + str(i): 0x40000175 for i in range(1, 3)},
|
||||
**{"Alluring Skull #" + str(i): 0x40000126 for i in range(1, 3)},
|
||||
**{"Undead Hunter Charm #" + str(i): 0x40000128 for i in range(1, 3)},
|
||||
**{"Duel Charm #" + str(i): 0x40000130 for i in range(1, 3)},
|
||||
**{"Rusted Coin #" + str(i): 0x400001C7 for i in range(1, 3)},
|
||||
**{"Rusted Gold Coin #" + str(i): 0x400001C9 for i in range(1, 4)},
|
||||
**{"Titanite Chunk #"+str(i): 0x400003EA for i in range(1, 17)},
|
||||
**{"Twinkling Titanite #"+str(i): 0x40000406 for i in range(1, 8)}
|
||||
}
|
||||
|
||||
dlc_progressive_locations = { #71
|
||||
**{"Large Soul of an Unknown Traveler $"+str(i): 0x40000194 for i in range(1, 10)},
|
||||
**{"Soul of a Weary Warrior $"+str(i): 0x40000197 for i in range(1, 6)},
|
||||
**{"Large Soul of a Weary Warrior $"+str(i): 0x40000198 for i in range(1, 7)},
|
||||
**{"Soul of a Crestfallen Knight $"+str(i): 0x40000199 for i in range(1, 7)},
|
||||
**{"Large Soul of a Crestfallen Knight $"+str(i): 0x4000019A for i in range(1, 4)},
|
||||
**{"Homeward Bone $"+str(i): 0x4000015E for i in range(1, 7)},
|
||||
**{"Large Titanite Shard $"+str(i): 0x400003E9 for i in range(1, 4)},
|
||||
**{"Titanite Chunk $"+str(i): 0x400003EA for i in range(1, 16)},
|
||||
**{"Twinkling Titanite $"+str(i): 0x40000406 for i in range(1, 6)},
|
||||
**{"Rusted Coin $"+str(i): 0x400001C7 for i in range(1, 4)},
|
||||
**{"Ember $"+str(i): 0x400001F4 for i in range(1, 11)}
|
||||
}
|
||||
|
||||
location_tables = [fire_link_shrine_table, firelink_shrine_bell_tower_table, high_wall_of_lothric, undead_settlement_table, road_of_sacrifice_table,
|
||||
cathedral_of_the_deep_table, farron_keep_table, catacombs_of_carthus_table, smouldering_lake_table, irithyll_of_the_boreal_valley_table,
|
||||
irithyll_dungeon_table, profaned_capital_table, anor_londo_table, lothric_castle_table, consumed_king_garden_table,
|
||||
grand_archives_table, untended_graves_table, archdragon_peak_table, progressive_locations, progressive_locations_2, progressive_locations_3,
|
||||
painted_world_table, dreg_heap_table, ringed_city_table, dlc_progressive_locations]
|
||||
|
||||
location_dictionary = {**fire_link_shrine_table, **firelink_shrine_bell_tower_table, **high_wall_of_lothric, **undead_settlement_table, **road_of_sacrifice_table,
|
||||
**cathedral_of_the_deep_table, **farron_keep_table, **catacombs_of_carthus_table, **smouldering_lake_table, **irithyll_of_the_boreal_valley_table,
|
||||
**irithyll_dungeon_table, **profaned_capital_table, **anor_londo_table, **lothric_castle_table, **consumed_king_garden_table,
|
||||
**grand_archives_table, **untended_graves_table, **archdragon_peak_table, **progressive_locations, **progressive_locations_2, **progressive_locations_3,
|
||||
**painted_world_table, **dreg_heap_table, **ringed_city_table, **dlc_progressive_locations}
|
||||
@@ -11,27 +11,32 @@
|
||||
|
||||
## General Concept
|
||||
|
||||
<span style="color:tomato">
|
||||
**This mod can ban you permanently from the FromSoftware servers if used online.**
|
||||
</span>
|
||||
The Dark Souls III AP Client is a dinput8.dll triggered when launching Dark Souls III. This .dll file will launch a command
|
||||
prompt where you can read information about your run and write any command to interact with the Archipelago server.
|
||||
|
||||
## Installation Procedures
|
||||
This client has only been tested with the Official Steam version of the game at version 1.15. It does not matter which DLCs are installed. However, you will have to downpatch your Dark Souls III installation from current patch.
|
||||
|
||||
<span style="color:tomato">
|
||||
**This mod can ban you permanently from the FromSoftware servers if used online.**
|
||||
</span>
|
||||
This client has only been tested with the Official Steam version of the game (v1.15/1.35) not matter which DLCs are installed.
|
||||
## Downpatching Dark Souls III
|
||||
|
||||
Follow instructions from the [speedsouls wiki](https://wiki.speedsouls.com/darksouls3:Downpatching) to download version 1.15. Your download command, including the correct depot and manifest ids, will be "download_depot 374320 374321 4471176929659548333"
|
||||
|
||||
## Installing the Archipelago mod
|
||||
|
||||
Get the dinput8.dll from the [Dark Souls III AP Client](https://github.com/Marechal-L/Dark-Souls-III-Archipelago-client/releases) and
|
||||
add it at the root folder of your game (e.g. "SteamLibrary\steamapps\common\DARK SOULS III\Game")
|
||||
|
||||
## Joining a MultiWorld Game
|
||||
|
||||
1. Run DarkSoulsIII.exe or run the game through Steam
|
||||
2. Type in "/connect {SERVER_IP}:{SERVER_PORT} {SLOT_NAME}" in the "Windows Command Prompt" that opened
|
||||
3. Once connected, create a new game, choose a class and wait for the others before starting
|
||||
4. You can quit and launch at anytime during a game
|
||||
1. Run Steam in offline mode, both to avoid being banned and to prevent Steam from updating the game files
|
||||
2. Launch Dark Souls III
|
||||
3. Type in "/connect {SERVER_IP}:{SERVER_PORT} {SLOT_NAME}" in the "Windows Command Prompt" that opened
|
||||
4. Once connected, create a new game, choose a class and wait for the others before starting
|
||||
5. You can quit and launch at anytime during a game
|
||||
|
||||
## Where do I get a config file?
|
||||
|
||||
The [Player Settings](/games/Dark%20Souls%20III/player-settings) page on the website allows you to
|
||||
configure your personal settings and export them into a config file
|
||||
configure your personal settings and export them into a config file.
|
||||
|
||||
@@ -3,6 +3,7 @@ import typing
|
||||
import math
|
||||
import threading
|
||||
|
||||
import settings
|
||||
from BaseClasses import Item, MultiWorld, Tutorial, ItemClassification
|
||||
from .Items import DKC3Item, ItemData, item_table, inventory_table, junk_table
|
||||
from .Locations import DKC3Location, all_locations, setup_locations
|
||||
@@ -17,6 +18,16 @@ from .Rom import LocalRom, patch_rom, get_base_rom_path, DKC3DeltaPatch
|
||||
import Patch
|
||||
|
||||
|
||||
class DK3Settings(settings.Group):
|
||||
class RomFile(settings.UserFilePath):
|
||||
"""File name of the DKC3 US rom"""
|
||||
copy_to = "Donkey Kong Country 3 - Dixie Kong's Double Trouble! (USA) (En,Fr).sfc"
|
||||
description = "DKC3 (US) ROM File"
|
||||
md5s = [DKC3DeltaPatch.hash]
|
||||
|
||||
rom_file: RomFile = RomFile(RomFile.copy_to)
|
||||
|
||||
|
||||
class DKC3Web(WebWorld):
|
||||
theme = "jungle"
|
||||
|
||||
@@ -40,6 +51,7 @@ class DKC3World(World):
|
||||
"""
|
||||
game: str = "Donkey Kong Country 3"
|
||||
option_definitions = dkc3_options
|
||||
settings: typing.ClassVar[DK3Settings]
|
||||
topology_present = False
|
||||
data_version = 2
|
||||
#hint_blacklist = {LocationName.rocket_rush_flag}
|
||||
@@ -74,7 +86,11 @@ class DKC3World(World):
|
||||
|
||||
return slot_data
|
||||
|
||||
def generate_basic(self):
|
||||
def create_regions(self):
|
||||
location_table = setup_locations(self.multiworld, self.player)
|
||||
create_regions(self.multiworld, self.player, location_table)
|
||||
|
||||
# Not generate basic
|
||||
self.topology_present = self.multiworld.level_shuffle[self.player].value
|
||||
itempool: typing.List[DKC3Item] = []
|
||||
|
||||
@@ -186,10 +202,6 @@ class DKC3World(World):
|
||||
er_hint_data[location.address] = world_names[world_index]
|
||||
multidata['er_hint_data'][self.player] = er_hint_data
|
||||
|
||||
def create_regions(self):
|
||||
location_table = setup_locations(self.multiworld, self.player)
|
||||
create_regions(self.multiworld, self.player, location_table)
|
||||
|
||||
def create_item(self, name: str, force_non_progression=False) -> Item:
|
||||
data = item_table[name]
|
||||
|
||||
@@ -204,5 +216,8 @@ class DKC3World(World):
|
||||
|
||||
return created_item
|
||||
|
||||
def get_filler_item_name(self) -> str:
|
||||
return self.multiworld.random.choice(list(junk_table.keys()))
|
||||
|
||||
def set_rules(self):
|
||||
set_rules(self.multiworld, self.player)
|
||||
|
||||
@@ -105,6 +105,8 @@ def set_basic_shuffled_items_rules(World_Options, player, world):
|
||||
lambda state: state.has("Sword", player) or state.has("Gun", player))
|
||||
set_rule(world.get_location("West Cave Sheep", player),
|
||||
lambda state: state.has("Sword", player) or state.has("Gun", player))
|
||||
set_rule(world.get_location("Gun", player),
|
||||
lambda state: state.has("Gun Pack", player))
|
||||
|
||||
if World_Options[Options.TimeIsMoney] == Options.TimeIsMoney.option_required:
|
||||
set_rule(world.get_location("Sword", player),
|
||||
|
||||
1172
worlds/doom_1993/Items.py
Normal file
1172
worlds/doom_1993/Items.py
Normal file
File diff suppressed because it is too large
Load Diff
3422
worlds/doom_1993/Locations.py
Normal file
3422
worlds/doom_1993/Locations.py
Normal file
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user