mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-03-23 03:23:23 -07:00
Compare commits
1 Commits
multiserve
...
oc2-progre
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b70a5b8dbd |
29
.github/workflows/build.yml
vendored
29
.github/workflows/build.yml
vendored
@@ -21,17 +21,12 @@ env:
|
||||
ENEMIZER_VERSION: 7.1
|
||||
APPIMAGETOOL_VERSION: 13
|
||||
|
||||
permissions: # permissions required for attestation
|
||||
id-token: 'write'
|
||||
attestations: 'write'
|
||||
|
||||
jobs:
|
||||
# build-release-macos: # LF volunteer
|
||||
|
||||
build-win: # RCs and releases may still be built and signed by hand
|
||||
build-win: # RCs will still be built and signed by hand
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
# - copy code below to release.yml -
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install python
|
||||
uses: actions/setup-python@v5
|
||||
@@ -70,18 +65,6 @@ jobs:
|
||||
$contents = Get-ChildItem -Path setups/*.exe -Force -Recurse
|
||||
$SETUP_NAME=$contents[0].Name
|
||||
echo "SETUP_NAME=$SETUP_NAME" >> $Env:GITHUB_ENV
|
||||
# - copy code above to release.yml -
|
||||
- name: Attest Build
|
||||
if: ${{ github.event_name == 'workflow_dispatch' }}
|
||||
uses: actions/attest-build-provenance@v2
|
||||
with:
|
||||
subject-path: |
|
||||
build/exe.*/ArchipelagoLauncher.exe
|
||||
build/exe.*/ArchipelagoLauncherDebug.exe
|
||||
build/exe.*/ArchipelagoGenerate.exe
|
||||
build/exe.*/ArchipelagoServer.exe
|
||||
dist/${{ env.ZIP_NAME }}
|
||||
setups/${{ env.SETUP_NAME }}
|
||||
- name: Check build loads expected worlds
|
||||
shell: bash
|
||||
run: |
|
||||
@@ -159,16 +142,6 @@ jobs:
|
||||
echo "APPIMAGE_NAME=$APPIMAGE_NAME" >> $GITHUB_ENV
|
||||
echo "TAR_NAME=$TAR_NAME" >> $GITHUB_ENV
|
||||
# - copy code above to release.yml -
|
||||
- name: Attest Build
|
||||
if: ${{ github.event_name == 'workflow_dispatch' }}
|
||||
uses: actions/attest-build-provenance@v2
|
||||
with:
|
||||
subject-path: |
|
||||
build/exe.*/ArchipelagoLauncher
|
||||
build/exe.*/ArchipelagoGenerate
|
||||
build/exe.*/ArchipelagoServer
|
||||
dist/${{ env.APPIMAGE_NAME }}*
|
||||
dist/${{ env.TAR_NAME }}
|
||||
- name: Build Again
|
||||
run: |
|
||||
source venv/bin/activate
|
||||
|
||||
83
.github/workflows/release.yml
vendored
83
.github/workflows/release.yml
vendored
@@ -11,11 +11,6 @@ env:
|
||||
ENEMIZER_VERSION: 7.1
|
||||
APPIMAGETOOL_VERSION: 13
|
||||
|
||||
permissions: # permissions required for attestation
|
||||
id-token: 'write'
|
||||
attestations: 'write'
|
||||
contents: 'write' # additionally required for release
|
||||
|
||||
jobs:
|
||||
create-release:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -31,79 +26,11 @@ jobs:
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# build-release-windows: # this is done by hand because of signing
|
||||
# build-release-macos: # LF volunteer
|
||||
|
||||
build-release-win:
|
||||
runs-on: windows-latest
|
||||
if: ${{ true }} # change to false to skip if release is built by hand
|
||||
needs: create-release
|
||||
steps:
|
||||
- name: Set env
|
||||
shell: bash
|
||||
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
|
||||
# - code below copied from build.yml -
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '~3.12.7'
|
||||
check-latest: true
|
||||
- name: Download run-time dependencies
|
||||
run: |
|
||||
Invoke-WebRequest -Uri https://github.com/Ijwu/Enemizer/releases/download/${Env:ENEMIZER_VERSION}/win-x64.zip -OutFile enemizer.zip
|
||||
Expand-Archive -Path enemizer.zip -DestinationPath EnemizerCLI -Force
|
||||
choco install innosetup --version=6.2.2 --allow-downgrade
|
||||
- name: Build
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
python setup.py build_exe --yes
|
||||
if ( $? -eq $false ) {
|
||||
Write-Error "setup.py failed!"
|
||||
exit 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
|
||||
7z a -mx=9 -mhe=on -ms "../dist/$ZIP_NAME" Archipelago
|
||||
Rename-Item Archipelago "exe.$NAME" # inno_setup.iss expects the original name
|
||||
- name: Build Setup
|
||||
run: |
|
||||
& "${env:ProgramFiles(x86)}\Inno Setup 6\iscc.exe" inno_setup.iss /DNO_SIGNTOOL
|
||||
if ( $? -eq $false ) {
|
||||
Write-Error "Building setup failed!"
|
||||
exit 1
|
||||
}
|
||||
$contents = Get-ChildItem -Path setups/*.exe -Force -Recurse
|
||||
$SETUP_NAME=$contents[0].Name
|
||||
echo "SETUP_NAME=$SETUP_NAME" >> $Env:GITHUB_ENV
|
||||
# - code above copied from build.yml -
|
||||
- name: Attest Build
|
||||
uses: actions/attest-build-provenance@v2
|
||||
with:
|
||||
subject-path: |
|
||||
build/exe.*/ArchipelagoLauncher.exe
|
||||
build/exe.*/ArchipelagoLauncherDebug.exe
|
||||
build/exe.*/ArchipelagoGenerate.exe
|
||||
build/exe.*/ArchipelagoServer.exe
|
||||
setups/*
|
||||
- name: Add to Release
|
||||
uses: softprops/action-gh-release@975c1b265e11dd76618af1c374e7981f9a6ff44a
|
||||
with:
|
||||
draft: true # see above
|
||||
prerelease: false
|
||||
name: Archipelago ${{ env.RELEASE_VERSION }}
|
||||
files: |
|
||||
setups/*
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
build-release-ubuntu2204:
|
||||
runs-on: ubuntu-22.04
|
||||
needs: create-release
|
||||
steps:
|
||||
- name: Set env
|
||||
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
|
||||
@@ -147,14 +74,6 @@ jobs:
|
||||
echo "APPIMAGE_NAME=$APPIMAGE_NAME" >> $GITHUB_ENV
|
||||
echo "TAR_NAME=$TAR_NAME" >> $GITHUB_ENV
|
||||
# - code above copied from build.yml -
|
||||
- name: Attest Build
|
||||
uses: actions/attest-build-provenance@v2
|
||||
with:
|
||||
subject-path: |
|
||||
build/exe.*/ArchipelagoLauncher
|
||||
build/exe.*/ArchipelagoGenerate
|
||||
build/exe.*/ArchipelagoServer
|
||||
dist/*
|
||||
- name: Add to Release
|
||||
uses: softprops/action-gh-release@975c1b265e11dd76618af1c374e7981f9a6ff44a
|
||||
with:
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import sys
|
||||
from worlds.ahit.Client import launch
|
||||
import Utils
|
||||
import ModuleUpdate
|
||||
@@ -6,4 +5,4 @@ ModuleUpdate.update()
|
||||
|
||||
if __name__ == "__main__":
|
||||
Utils.init_logging("AHITClient", exception_logger="Client")
|
||||
launch(*sys.argv[1:])
|
||||
launch()
|
||||
|
||||
@@ -9,9 +9,8 @@ from argparse import Namespace
|
||||
from collections import Counter, deque
|
||||
from collections.abc import Collection, MutableSequence
|
||||
from enum import IntEnum, IntFlag
|
||||
from typing import (AbstractSet, Any, Callable, ClassVar, Dict, Iterable, Iterator, List, Literal, Mapping, NamedTuple,
|
||||
from typing import (AbstractSet, Any, Callable, ClassVar, Dict, Iterable, Iterator, List, Mapping, NamedTuple,
|
||||
Optional, Protocol, Set, Tuple, Union, TYPE_CHECKING)
|
||||
import dataclasses
|
||||
|
||||
from typing_extensions import NotRequired, TypedDict
|
||||
|
||||
@@ -55,21 +54,12 @@ class HasNameAndPlayer(Protocol):
|
||||
player: int
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class PlandoItemBlock:
|
||||
player: int
|
||||
from_pool: bool
|
||||
force: bool | Literal["silent"]
|
||||
worlds: set[int] = dataclasses.field(default_factory=set)
|
||||
items: list[str] = dataclasses.field(default_factory=list)
|
||||
locations: list[str] = dataclasses.field(default_factory=list)
|
||||
resolved_locations: list[Location] = dataclasses.field(default_factory=list)
|
||||
count: dict[str, int] = dataclasses.field(default_factory=dict)
|
||||
|
||||
|
||||
class MultiWorld():
|
||||
debug_types = False
|
||||
player_name: Dict[int, str]
|
||||
plando_texts: List[Dict[str, str]]
|
||||
plando_items: List[List[Dict[str, Any]]]
|
||||
plando_connections: List
|
||||
worlds: Dict[int, "AutoWorld.World"]
|
||||
groups: Dict[int, Group]
|
||||
regions: RegionManager
|
||||
@@ -93,8 +83,6 @@ class MultiWorld():
|
||||
start_location_hints: Dict[int, Options.StartLocationHints]
|
||||
item_links: Dict[int, Options.ItemLinks]
|
||||
|
||||
plando_item_blocks: Dict[int, List[PlandoItemBlock]]
|
||||
|
||||
game: Dict[int, str]
|
||||
|
||||
random: random.Random
|
||||
@@ -172,12 +160,13 @@ class MultiWorld():
|
||||
self.local_early_items = {player: {} for player in self.player_ids}
|
||||
self.indirect_connections = {}
|
||||
self.start_inventory_from_pool: Dict[int, Options.StartInventoryPool] = {}
|
||||
self.plando_item_blocks = {}
|
||||
|
||||
for player in range(1, players + 1):
|
||||
def set_player_attr(attr: str, val) -> None:
|
||||
self.__dict__.setdefault(attr, {})[player] = val
|
||||
set_player_attr('plando_item_blocks', [])
|
||||
set_player_attr('plando_items', [])
|
||||
set_player_attr('plando_texts', {})
|
||||
set_player_attr('plando_connections', [])
|
||||
set_player_attr('game', "Archipelago")
|
||||
set_player_attr('completion_condition', lambda state: True)
|
||||
self.worlds = {}
|
||||
@@ -438,8 +427,7 @@ class MultiWorld():
|
||||
def get_location(self, location_name: str, player: int) -> Location:
|
||||
return self.regions.location_cache[player][location_name]
|
||||
|
||||
def get_all_state(self, use_cache: bool, allow_partial_entrances: bool = False,
|
||||
collect_pre_fill_items: bool = True) -> CollectionState:
|
||||
def get_all_state(self, use_cache: bool, allow_partial_entrances: bool = False) -> CollectionState:
|
||||
cached = getattr(self, "_all_state", None)
|
||||
if use_cache and cached:
|
||||
return cached.copy()
|
||||
@@ -448,11 +436,10 @@ class MultiWorld():
|
||||
|
||||
for item in self.itempool:
|
||||
self.worlds[item.player].collect(ret, item)
|
||||
if collect_pre_fill_items:
|
||||
for player in self.player_ids:
|
||||
subworld = self.worlds[player]
|
||||
for item in subworld.get_pre_fill_items():
|
||||
subworld.collect(ret, item)
|
||||
for player in self.player_ids:
|
||||
subworld = self.worlds[player]
|
||||
for item in subworld.get_pre_fill_items():
|
||||
subworld.collect(ret, item)
|
||||
ret.sweep_for_advancements()
|
||||
|
||||
if use_cache:
|
||||
|
||||
@@ -196,11 +196,25 @@ class CommonContext:
|
||||
self.lookup_type: typing.Literal["item", "location"] = lookup_type
|
||||
self._unknown_item: typing.Callable[[int], str] = lambda key: f"Unknown {lookup_type} (ID: {key})"
|
||||
self._archipelago_lookup: typing.Dict[int, str] = {}
|
||||
self._flat_store: typing.Dict[int, str] = Utils.KeyedDefaultDict(self._unknown_item)
|
||||
self._game_store: typing.Dict[str, typing.ChainMap[int, str]] = collections.defaultdict(
|
||||
lambda: collections.ChainMap(self._archipelago_lookup, Utils.KeyedDefaultDict(self._unknown_item)))
|
||||
self.warned: bool = False
|
||||
|
||||
# noinspection PyTypeChecker
|
||||
def __getitem__(self, key: str) -> typing.Mapping[int, str]:
|
||||
# TODO: In a future version (0.6.0?) this should be simplified by removing implicit id lookups support.
|
||||
if isinstance(key, int):
|
||||
if not self.warned:
|
||||
# Use warnings instead of logger to avoid deprecation message from appearing on user side.
|
||||
self.warned = True
|
||||
warnings.warn(f"Implicit name lookup by id only is deprecated and only supported to maintain "
|
||||
f"backwards compatibility for now. If multiple games share the same id for a "
|
||||
f"{self.lookup_type}, name could be incorrect. Please use "
|
||||
f"`{self.lookup_type}_names.lookup_in_game()` or "
|
||||
f"`{self.lookup_type}_names.lookup_in_slot()` instead.")
|
||||
return self._flat_store[key] # type: ignore
|
||||
|
||||
return self._game_store[key]
|
||||
|
||||
def __len__(self) -> int:
|
||||
@@ -240,6 +254,7 @@ class CommonContext:
|
||||
id_to_name_lookup_table = Utils.KeyedDefaultDict(self._unknown_item)
|
||||
id_to_name_lookup_table.update({code: name for name, code in name_to_id_lookup_table.items()})
|
||||
self._game_store[game] = collections.ChainMap(self._archipelago_lookup, id_to_name_lookup_table)
|
||||
self._flat_store.update(id_to_name_lookup_table) # Only needed for legacy lookup method.
|
||||
if game == "Archipelago":
|
||||
# Keep track of the Archipelago data package separately so if it gets updated in a custom datapackage,
|
||||
# it updates in all chain maps automatically.
|
||||
@@ -341,6 +356,7 @@ class CommonContext:
|
||||
|
||||
self.item_names = self.NameLookupDict(self, "item")
|
||||
self.location_names = self.NameLookupDict(self, "location")
|
||||
self.versions = {}
|
||||
self.checksums = {}
|
||||
|
||||
self.jsontotextparser = JSONtoTextParser(self)
|
||||
@@ -555,6 +571,7 @@ class CommonContext:
|
||||
|
||||
# DataPackage
|
||||
async def prepare_data_package(self, relevant_games: typing.Set[str],
|
||||
remote_date_package_versions: typing.Dict[str, int],
|
||||
remote_data_package_checksums: typing.Dict[str, str]):
|
||||
"""Validate that all data is present for the current multiworld.
|
||||
Download, assimilate and cache missing data from the server."""
|
||||
@@ -563,26 +580,33 @@ class CommonContext:
|
||||
|
||||
needed_updates: typing.Set[str] = set()
|
||||
for game in relevant_games:
|
||||
if game not in remote_data_package_checksums:
|
||||
if game not in remote_date_package_versions and game not in remote_data_package_checksums:
|
||||
continue
|
||||
|
||||
remote_version: int = remote_date_package_versions.get(game, 0)
|
||||
remote_checksum: typing.Optional[str] = remote_data_package_checksums.get(game)
|
||||
|
||||
if not remote_checksum: # custom data package and no checksum for this game
|
||||
if remote_version == 0 and not remote_checksum: # custom data package and no checksum for this game
|
||||
needed_updates.add(game)
|
||||
continue
|
||||
|
||||
cached_version: int = self.versions.get(game, 0)
|
||||
cached_checksum: typing.Optional[str] = self.checksums.get(game)
|
||||
# no action required if cached version is new enough
|
||||
if remote_checksum != cached_checksum:
|
||||
if (not remote_checksum and (remote_version > cached_version or remote_version == 0)) \
|
||||
or remote_checksum != cached_checksum:
|
||||
local_version: int = network_data_package["games"].get(game, {}).get("version", 0)
|
||||
local_checksum: typing.Optional[str] = network_data_package["games"].get(game, {}).get("checksum")
|
||||
if remote_checksum == local_checksum:
|
||||
if ((remote_checksum or remote_version <= local_version and remote_version != 0)
|
||||
and remote_checksum == local_checksum):
|
||||
self.update_game(network_data_package["games"][game], game)
|
||||
else:
|
||||
cached_game = Utils.load_data_package_for_checksum(game, remote_checksum)
|
||||
cache_version: int = cached_game.get("version", 0)
|
||||
cache_checksum: typing.Optional[str] = cached_game.get("checksum")
|
||||
# download remote version if cache is not new enough
|
||||
if remote_checksum != cache_checksum:
|
||||
if (not remote_checksum and (remote_version > cache_version or remote_version == 0)) \
|
||||
or remote_checksum != cache_checksum:
|
||||
needed_updates.add(game)
|
||||
else:
|
||||
self.update_game(cached_game, game)
|
||||
@@ -592,6 +616,7 @@ class CommonContext:
|
||||
def update_game(self, game_package: dict, game: str):
|
||||
self.item_names.update_game(game, game_package["item_name_to_id"])
|
||||
self.location_names.update_game(game, game_package["location_name_to_id"])
|
||||
self.versions[game] = game_package.get("version", 0)
|
||||
self.checksums[game] = game_package.get("checksum")
|
||||
|
||||
def update_data_package(self, data_package: dict):
|
||||
@@ -862,8 +887,9 @@ async def process_server_cmd(ctx: CommonContext, args: dict):
|
||||
logger.info(' %s (Player %d)' % (network_player.alias, network_player.slot))
|
||||
|
||||
# update data package
|
||||
data_package_versions = args.get("datapackage_versions", {})
|
||||
data_package_checksums = args.get("datapackage_checksums", {})
|
||||
await ctx.prepare_data_package(set(args["games"]), data_package_checksums)
|
||||
await ctx.prepare_data_package(set(args["games"]), data_package_versions, data_package_checksums)
|
||||
|
||||
await ctx.server_auth(args['password'])
|
||||
|
||||
|
||||
12
FactorioClient.py
Normal file
12
FactorioClient.py
Normal file
@@ -0,0 +1,12 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import ModuleUpdate
|
||||
ModuleUpdate.update()
|
||||
|
||||
from worlds.factorio.Client import check_stdin, launch
|
||||
import Utils
|
||||
|
||||
if __name__ == "__main__":
|
||||
Utils.init_logging("FactorioClient", exception_logger="Client")
|
||||
check_stdin()
|
||||
launch()
|
||||
349
Fill.py
349
Fill.py
@@ -4,7 +4,7 @@ import logging
|
||||
import typing
|
||||
from collections import Counter, deque
|
||||
|
||||
from BaseClasses import CollectionState, Item, Location, LocationProgressType, MultiWorld, PlandoItemBlock
|
||||
from BaseClasses import CollectionState, Item, Location, LocationProgressType, MultiWorld
|
||||
from Options import Accessibility
|
||||
|
||||
from worlds.AutoWorld import call_all
|
||||
@@ -100,7 +100,7 @@ def fill_restrictive(multiworld: MultiWorld, base_state: CollectionState, locati
|
||||
# if minimal accessibility, only check whether location is reachable if game not beatable
|
||||
if multiworld.worlds[item_to_place.player].options.accessibility == Accessibility.option_minimal:
|
||||
perform_access_check = not multiworld.has_beaten_game(maximum_exploration_state,
|
||||
item_to_place.player) \
|
||||
item_to_place.player) \
|
||||
if single_player_placement else not has_beaten_game
|
||||
else:
|
||||
perform_access_check = True
|
||||
@@ -242,7 +242,7 @@ def remaining_fill(multiworld: MultiWorld,
|
||||
unplaced_items: typing.List[Item] = []
|
||||
placements: typing.List[Location] = []
|
||||
swapped_items: typing.Counter[typing.Tuple[int, str]] = Counter()
|
||||
total = min(len(itempool), len(locations))
|
||||
total = min(len(itempool), len(locations))
|
||||
placed = 0
|
||||
|
||||
# Optimisation: Decide whether to do full location.can_fill check (respect excluded), or only check the item rule
|
||||
@@ -343,10 +343,8 @@ def fast_fill(multiworld: MultiWorld,
|
||||
|
||||
def accessibility_corrections(multiworld: MultiWorld, state: CollectionState, locations, pool=[]):
|
||||
maximum_exploration_state = sweep_from_pool(state, pool)
|
||||
minimal_players = {player for player in multiworld.player_ids if
|
||||
multiworld.worlds[player].options.accessibility == "minimal"}
|
||||
unreachable_locations = [location for location in multiworld.get_locations() if
|
||||
location.player in minimal_players and
|
||||
minimal_players = {player for player in multiworld.player_ids if multiworld.worlds[player].options.accessibility == "minimal"}
|
||||
unreachable_locations = [location for location in multiworld.get_locations() if location.player in minimal_players and
|
||||
not location.can_reach(maximum_exploration_state)]
|
||||
for location in unreachable_locations:
|
||||
if (location.item is not None and location.item.advancement and location.address is not None and not
|
||||
@@ -367,7 +365,7 @@ def inaccessible_location_rules(multiworld: MultiWorld, state: CollectionState,
|
||||
unreachable_locations = [location for location in locations if not location.can_reach(maximum_exploration_state)]
|
||||
if unreachable_locations:
|
||||
def forbid_important_item_rule(item: Item):
|
||||
return not ((item.classification & 0b0011) and multiworld.worlds[item.player].options.accessibility != "minimal")
|
||||
return not ((item.classification & 0b0011) and multiworld.worlds[item.player].options.accessibility != 'minimal')
|
||||
|
||||
for location in unreachable_locations:
|
||||
add_item_rule(location, forbid_important_item_rule)
|
||||
@@ -679,9 +677,9 @@ def balance_multiworld_progression(multiworld: MultiWorld) -> None:
|
||||
if multiworld.worlds[player].options.progression_balancing > 0
|
||||
}
|
||||
if not balanceable_players:
|
||||
logging.info("Skipping multiworld progression balancing.")
|
||||
logging.info('Skipping multiworld progression balancing.')
|
||||
else:
|
||||
logging.info(f"Balancing multiworld progression for {len(balanceable_players)} Players.")
|
||||
logging.info(f'Balancing multiworld progression for {len(balanceable_players)} Players.')
|
||||
logging.debug(balanceable_players)
|
||||
state: CollectionState = CollectionState(multiworld)
|
||||
checked_locations: typing.Set[Location] = set()
|
||||
@@ -779,7 +777,7 @@ def balance_multiworld_progression(multiworld: MultiWorld) -> None:
|
||||
if player in threshold_percentages):
|
||||
break
|
||||
elif not balancing_sphere:
|
||||
raise RuntimeError("Not all required items reachable. Something went terribly wrong here.")
|
||||
raise RuntimeError('Not all required items reachable. Something went terribly wrong here.')
|
||||
# Gather a set of locations which we can swap items into
|
||||
unlocked_locations: typing.Dict[int, typing.Set[Location]] = collections.defaultdict(set)
|
||||
for l in unchecked_locations:
|
||||
@@ -795,8 +793,8 @@ def balance_multiworld_progression(multiworld: MultiWorld) -> None:
|
||||
testing = items_to_test.pop()
|
||||
reducing_state = state.copy()
|
||||
for location in itertools.chain((
|
||||
l for l in items_to_replace
|
||||
if l.item.player == player
|
||||
l for l in items_to_replace
|
||||
if l.item.player == player
|
||||
), items_to_test):
|
||||
reducing_state.collect(location.item, True, location)
|
||||
|
||||
@@ -869,30 +867,52 @@ def swap_location_item(location_1: Location, location_2: Location, check_locked:
|
||||
location_2.item.location = location_2
|
||||
|
||||
|
||||
def parse_planned_blocks(multiworld: MultiWorld) -> dict[int, list[PlandoItemBlock]]:
|
||||
def warn(warning: str, force: bool | str) -> None:
|
||||
if isinstance(force, bool):
|
||||
logging.warning(f"{warning}")
|
||||
def distribute_planned(multiworld: MultiWorld) -> None:
|
||||
def warn(warning: str, force: typing.Union[bool, str]) -> None:
|
||||
if force in [True, 'fail', 'failure', 'none', False, 'warn', 'warning']:
|
||||
logging.warning(f'{warning}')
|
||||
else:
|
||||
logging.debug(f"{warning}")
|
||||
logging.debug(f'{warning}')
|
||||
|
||||
def failed(warning: str, force: bool | str) -> None:
|
||||
if force is True:
|
||||
def failed(warning: str, force: typing.Union[bool, str]) -> None:
|
||||
if force in [True, 'fail', 'failure']:
|
||||
raise Exception(warning)
|
||||
else:
|
||||
warn(warning, force)
|
||||
|
||||
swept_state = multiworld.state.copy()
|
||||
swept_state.sweep_for_advancements()
|
||||
reachable = frozenset(multiworld.get_reachable_locations(swept_state))
|
||||
early_locations: typing.Dict[int, typing.List[str]] = collections.defaultdict(list)
|
||||
non_early_locations: typing.Dict[int, typing.List[str]] = collections.defaultdict(list)
|
||||
for loc in multiworld.get_unfilled_locations():
|
||||
if loc in reachable:
|
||||
early_locations[loc.player].append(loc.name)
|
||||
else: # not reachable with swept state
|
||||
non_early_locations[loc.player].append(loc.name)
|
||||
|
||||
world_name_lookup = multiworld.world_name_lookup
|
||||
|
||||
plando_blocks: dict[int, list[PlandoItemBlock]] = dict()
|
||||
player_ids: set[int] = set(multiworld.player_ids)
|
||||
block_value = typing.Union[typing.List[str], typing.Dict[str, typing.Any], str]
|
||||
plando_blocks: typing.List[typing.Dict[str, typing.Any]] = []
|
||||
player_ids = set(multiworld.player_ids)
|
||||
for player in player_ids:
|
||||
plando_blocks[player] = []
|
||||
for block in multiworld.worlds[player].options.plando_items:
|
||||
new_block: PlandoItemBlock = PlandoItemBlock(player, block.from_pool, block.force)
|
||||
target_world = block.world
|
||||
for block in multiworld.plando_items[player]:
|
||||
block['player'] = player
|
||||
if 'force' not in block:
|
||||
block['force'] = 'silent'
|
||||
if 'from_pool' not in block:
|
||||
block['from_pool'] = True
|
||||
elif not isinstance(block['from_pool'], bool):
|
||||
from_pool_type = type(block['from_pool'])
|
||||
raise Exception(f'Plando "from_pool" has to be boolean, not {from_pool_type} for player {player}.')
|
||||
if 'world' not in block:
|
||||
target_world = False
|
||||
else:
|
||||
target_world = block['world']
|
||||
|
||||
if target_world is False or multiworld.players == 1: # target own world
|
||||
worlds: set[int] = {player}
|
||||
worlds: typing.Set[int] = {player}
|
||||
elif target_world is True: # target any worlds besides own
|
||||
worlds = set(multiworld.player_ids) - {player}
|
||||
elif target_world is None: # target all worlds
|
||||
@@ -902,197 +922,172 @@ def parse_planned_blocks(multiworld: MultiWorld) -> dict[int, list[PlandoItemBlo
|
||||
for listed_world in target_world:
|
||||
if listed_world not in world_name_lookup:
|
||||
failed(f"Cannot place item to {target_world}'s world as that world does not exist.",
|
||||
block.force)
|
||||
block['force'])
|
||||
continue
|
||||
worlds.add(world_name_lookup[listed_world])
|
||||
elif type(target_world) == int: # target world by slot number
|
||||
if target_world not in range(1, multiworld.players + 1):
|
||||
failed(
|
||||
f"Cannot place item in world {target_world} as it is not in range of (1, {multiworld.players})",
|
||||
block.force)
|
||||
block['force'])
|
||||
continue
|
||||
worlds = {target_world}
|
||||
else: # target world by slot name
|
||||
if target_world not in world_name_lookup:
|
||||
failed(f"Cannot place item to {target_world}'s world as that world does not exist.",
|
||||
block.force)
|
||||
block['force'])
|
||||
continue
|
||||
worlds = {world_name_lookup[target_world]}
|
||||
new_block.worlds = worlds
|
||||
block['world'] = worlds
|
||||
|
||||
items: list[str] | dict[str, typing.Any] = block.items
|
||||
items: block_value = []
|
||||
if "items" in block:
|
||||
items = block["items"]
|
||||
if 'count' not in block:
|
||||
block['count'] = False
|
||||
elif "item" in block:
|
||||
items = block["item"]
|
||||
if 'count' not in block:
|
||||
block['count'] = 1
|
||||
else:
|
||||
failed("You must specify at least one item to place items with plando.", block['force'])
|
||||
continue
|
||||
if isinstance(items, dict):
|
||||
item_list: list[str] = []
|
||||
item_list: typing.List[str] = []
|
||||
for key, value in items.items():
|
||||
if value is True:
|
||||
value = multiworld.itempool.count(multiworld.worlds[player].create_item(key))
|
||||
item_list += [key] * value
|
||||
items = item_list
|
||||
new_block.items = items
|
||||
if isinstance(items, str):
|
||||
items = [items]
|
||||
block['items'] = items
|
||||
|
||||
locations: list[str] = block.locations
|
||||
locations: block_value = []
|
||||
if 'location' in block:
|
||||
locations = block['location'] # just allow 'location' to keep old yamls compatible
|
||||
elif 'locations' in block:
|
||||
locations = block['locations']
|
||||
if isinstance(locations, str):
|
||||
locations = [locations]
|
||||
|
||||
locations_from_groups: list[str] = []
|
||||
resolved_locations: list[Location] = []
|
||||
for target_player in worlds:
|
||||
world_locations = multiworld.get_unfilled_locations(target_player)
|
||||
for group in multiworld.worlds[target_player].location_name_groups:
|
||||
if group in locations:
|
||||
locations_from_groups.extend(multiworld.worlds[target_player].location_name_groups[group])
|
||||
resolved_locations.extend(location for location in world_locations
|
||||
if location.name in [*locations, *locations_from_groups])
|
||||
new_block.locations = sorted(dict.fromkeys(locations))
|
||||
new_block.resolved_locations = sorted(set(resolved_locations))
|
||||
if isinstance(locations, dict):
|
||||
location_list = []
|
||||
for key, value in locations.items():
|
||||
location_list += [key] * value
|
||||
locations = location_list
|
||||
|
||||
count = block.count
|
||||
if not count:
|
||||
count = len(new_block.items)
|
||||
if isinstance(count, int):
|
||||
count = {"min": count, "max": count}
|
||||
if "min" not in count:
|
||||
count["min"] = 0
|
||||
if "max" not in count:
|
||||
count["max"] = len(new_block.items)
|
||||
|
||||
new_block.count = count
|
||||
plando_blocks[player].append(new_block)
|
||||
|
||||
return plando_blocks
|
||||
|
||||
|
||||
def resolve_early_locations_for_planned(multiworld: MultiWorld):
|
||||
def warn(warning: str, force: bool | str) -> None:
|
||||
if isinstance(force, bool):
|
||||
logging.warning(f"{warning}")
|
||||
else:
|
||||
logging.debug(f"{warning}")
|
||||
|
||||
def failed(warning: str, force: bool | str) -> None:
|
||||
if force is True:
|
||||
raise Exception(warning)
|
||||
else:
|
||||
warn(warning, force)
|
||||
|
||||
swept_state = multiworld.state.copy()
|
||||
swept_state.sweep_for_advancements()
|
||||
reachable = frozenset(multiworld.get_reachable_locations(swept_state))
|
||||
early_locations: dict[int, list[Location]] = collections.defaultdict(list)
|
||||
non_early_locations: dict[int, list[Location]] = collections.defaultdict(list)
|
||||
for loc in multiworld.get_unfilled_locations():
|
||||
if loc in reachable:
|
||||
early_locations[loc.player].append(loc)
|
||||
else: # not reachable with swept state
|
||||
non_early_locations[loc.player].append(loc)
|
||||
|
||||
for player in multiworld.plando_item_blocks:
|
||||
removed = []
|
||||
for block in multiworld.plando_item_blocks[player]:
|
||||
locations = block.locations
|
||||
resolved_locations = block.resolved_locations
|
||||
worlds = block.worlds
|
||||
if "early_locations" in locations:
|
||||
locations.remove("early_locations")
|
||||
for target_player in worlds:
|
||||
resolved_locations += early_locations[target_player]
|
||||
locations += early_locations[target_player]
|
||||
if "non_early_locations" in locations:
|
||||
locations.remove("non_early_locations")
|
||||
for target_player in worlds:
|
||||
resolved_locations += non_early_locations[target_player]
|
||||
locations += non_early_locations[target_player]
|
||||
|
||||
if block.count["max"] > len(block.items):
|
||||
count = block.count["max"]
|
||||
failed(f"Plando count {count} greater than items specified", block.force)
|
||||
block.count["max"] = len(block.items)
|
||||
if block.count["min"] > len(block.items):
|
||||
block.count["min"] = len(block.items)
|
||||
if block.count["max"] > len(block.resolved_locations) > 0:
|
||||
count = block.count["max"]
|
||||
failed(f"Plando count {count} greater than locations specified", block.force)
|
||||
block.count["max"] = len(block.resolved_locations)
|
||||
if block.count["min"] > len(block.resolved_locations):
|
||||
block.count["min"] = len(block.resolved_locations)
|
||||
block.count["target"] = multiworld.random.randint(block.count["min"],
|
||||
block.count["max"])
|
||||
block['locations'] = list(dict.fromkeys(locations))
|
||||
|
||||
if not block.count["target"]:
|
||||
removed.append(block)
|
||||
if not block['count']:
|
||||
block['count'] = (min(len(block['items']), len(block['locations'])) if
|
||||
len(block['locations']) > 0 else len(block['items']))
|
||||
if isinstance(block['count'], int):
|
||||
block['count'] = {'min': block['count'], 'max': block['count']}
|
||||
if 'min' not in block['count']:
|
||||
block['count']['min'] = 0
|
||||
if 'max' not in block['count']:
|
||||
block['count']['max'] = (min(len(block['items']), len(block['locations'])) if
|
||||
len(block['locations']) > 0 else len(block['items']))
|
||||
if block['count']['max'] > len(block['items']):
|
||||
count = block['count']
|
||||
failed(f"Plando count {count} greater than items specified", block['force'])
|
||||
block['count'] = len(block['items'])
|
||||
if block['count']['max'] > len(block['locations']) > 0:
|
||||
count = block['count']
|
||||
failed(f"Plando count {count} greater than locations specified", block['force'])
|
||||
block['count'] = len(block['locations'])
|
||||
block['count']['target'] = multiworld.random.randint(block['count']['min'], block['count']['max'])
|
||||
|
||||
for block in removed:
|
||||
multiworld.plando_item_blocks[player].remove(block)
|
||||
|
||||
|
||||
def distribute_planned_blocks(multiworld: MultiWorld, plando_blocks: list[PlandoItemBlock]):
|
||||
def warn(warning: str, force: bool | str) -> None:
|
||||
if isinstance(force, bool):
|
||||
logging.warning(f"{warning}")
|
||||
else:
|
||||
logging.debug(f"{warning}")
|
||||
|
||||
def failed(warning: str, force: bool | str) -> None:
|
||||
if force is True:
|
||||
raise Exception(warning)
|
||||
else:
|
||||
warn(warning, force)
|
||||
if block['count']['target'] > 0:
|
||||
plando_blocks.append(block)
|
||||
|
||||
# shuffle, but then sort blocks by number of locations minus number of items,
|
||||
# so less-flexible blocks get priority
|
||||
multiworld.random.shuffle(plando_blocks)
|
||||
plando_blocks.sort(key=lambda block: (len(block.resolved_locations) - block.count["target"]
|
||||
if len(block.resolved_locations) > 0
|
||||
else len(multiworld.get_unfilled_locations(block.player)) -
|
||||
block.count["target"]))
|
||||
plando_blocks.sort(key=lambda block: (len(block['locations']) - block['count']['target']
|
||||
if len(block['locations']) > 0
|
||||
else len(multiworld.get_unfilled_locations(player)) - block['count']['target']))
|
||||
|
||||
for placement in plando_blocks:
|
||||
player = placement.player
|
||||
player = placement['player']
|
||||
try:
|
||||
worlds = placement.worlds
|
||||
locations = placement.resolved_locations
|
||||
items = placement.items
|
||||
maxcount = placement.count["target"]
|
||||
from_pool = placement.from_pool
|
||||
worlds = placement['world']
|
||||
locations = placement['locations']
|
||||
items = placement['items']
|
||||
maxcount = placement['count']['target']
|
||||
from_pool = placement['from_pool']
|
||||
|
||||
item_candidates = []
|
||||
if from_pool:
|
||||
instances = [item for item in multiworld.itempool if item.player == player and item.name in items]
|
||||
for item in multiworld.random.sample(items, maxcount):
|
||||
candidate = next((i for i in instances if i.name == item), None)
|
||||
if candidate is None:
|
||||
warn(f"Could not remove {item} from pool for {multiworld.player_name[player]} as "
|
||||
f"it's already missing from it", placement.force)
|
||||
candidate = multiworld.worlds[player].create_item(item)
|
||||
else:
|
||||
multiworld.itempool.remove(candidate)
|
||||
instances.remove(candidate)
|
||||
item_candidates.append(candidate)
|
||||
else:
|
||||
item_candidates = [multiworld.worlds[player].create_item(item)
|
||||
for item in multiworld.random.sample(items, maxcount)]
|
||||
if any(item.code is None for item in item_candidates) \
|
||||
and not all(item.code is None for item in item_candidates):
|
||||
failed(f"Plando block for player {player} ({multiworld.player_name[player]}) contains both "
|
||||
f"event items and non-event items. "
|
||||
f"Event items: {[item for item in item_candidates if item.code is None]}, "
|
||||
f"Non-event items: {[item for item in item_candidates if item.code is not None]}",
|
||||
placement.force)
|
||||
continue
|
||||
else:
|
||||
is_real = item_candidates[0].code is not None
|
||||
candidates = [candidate for candidate in locations if candidate.item is None
|
||||
and bool(candidate.address) == is_real]
|
||||
candidates = list(multiworld.get_unfilled_locations_for_players(locations, sorted(worlds)))
|
||||
multiworld.random.shuffle(candidates)
|
||||
allstate = multiworld.get_all_state(False)
|
||||
mincount = placement.count["min"]
|
||||
allowed_margin = len(item_candidates) - mincount
|
||||
fill_restrictive(multiworld, allstate, candidates, item_candidates, lock=True,
|
||||
allow_partial=True, name="Plando Main Fill")
|
||||
multiworld.random.shuffle(items)
|
||||
count = 0
|
||||
err: typing.List[str] = []
|
||||
successful_pairs: typing.List[typing.Tuple[int, Item, Location]] = []
|
||||
claimed_indices: typing.Set[typing.Optional[int]] = set()
|
||||
for item_name in items:
|
||||
index_to_delete: typing.Optional[int] = None
|
||||
if from_pool:
|
||||
try:
|
||||
# If from_pool, try to find an existing item with this name & player in the itempool and use it
|
||||
index_to_delete, item = next(
|
||||
(i, item) for i, item in enumerate(multiworld.itempool)
|
||||
if item.player == player and item.name == item_name and i not in claimed_indices
|
||||
)
|
||||
except StopIteration:
|
||||
warn(
|
||||
f"Could not remove {item_name} from pool for {multiworld.player_name[player]} as it's already missing from it.",
|
||||
placement['force'])
|
||||
item = multiworld.worlds[player].create_item(item_name)
|
||||
else:
|
||||
item = multiworld.worlds[player].create_item(item_name)
|
||||
|
||||
for location in reversed(candidates):
|
||||
if (location.address is None) == (item.code is None): # either both None or both not None
|
||||
if not location.item:
|
||||
if location.item_rule(item):
|
||||
if location.can_fill(multiworld.state, item, False):
|
||||
successful_pairs.append((index_to_delete, item, location))
|
||||
claimed_indices.add(index_to_delete)
|
||||
candidates.remove(location)
|
||||
count = count + 1
|
||||
break
|
||||
else:
|
||||
err.append(f"Can't place item at {location} due to fill condition not met.")
|
||||
else:
|
||||
err.append(f"{item_name} not allowed at {location}.")
|
||||
else:
|
||||
err.append(f"Cannot place {item_name} into already filled location {location}.")
|
||||
else:
|
||||
err.append(f"Mismatch between {item_name} and {location}, only one is an event.")
|
||||
|
||||
if count == maxcount:
|
||||
break
|
||||
if count < placement['count']['min']:
|
||||
m = placement['count']['min']
|
||||
failed(
|
||||
f"Plando block failed to place {m - count} of {m} item(s) for {multiworld.player_name[player]}, error(s): {' '.join(err)}",
|
||||
placement['force'])
|
||||
|
||||
# Sort indices in reverse so we can remove them one by one
|
||||
successful_pairs = sorted(successful_pairs, key=lambda successful_pair: successful_pair[0] or 0, reverse=True)
|
||||
|
||||
for (index, item, location) in successful_pairs:
|
||||
multiworld.push_item(location, item, collect=False)
|
||||
location.locked = True
|
||||
logging.debug(f"Plando placed {item} at {location}")
|
||||
if index is not None: # If this item is from_pool and was found in the pool, remove it.
|
||||
multiworld.itempool.pop(index)
|
||||
|
||||
if len(item_candidates) > allowed_margin:
|
||||
failed(f"Could not place {len(item_candidates)} "
|
||||
f"of {mincount + allowed_margin} item(s) "
|
||||
f"for {multiworld.player_name[player]}, "
|
||||
f"remaining items: {item_candidates}",
|
||||
placement.force)
|
||||
if from_pool:
|
||||
multiworld.itempool.extend([item for item in item_candidates if item.code is not None])
|
||||
except Exception as e:
|
||||
raise Exception(
|
||||
f"Error running plando for player {player} ({multiworld.player_name[player]})") from e
|
||||
|
||||
43
Generate.py
43
Generate.py
@@ -10,8 +10,8 @@ import sys
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
from collections import Counter
|
||||
from typing import Any, Dict, Tuple, Union
|
||||
from itertools import chain
|
||||
from typing import Any
|
||||
|
||||
import ModuleUpdate
|
||||
|
||||
@@ -77,7 +77,7 @@ def get_seed_name(random_source) -> str:
|
||||
return f"{random_source.randint(0, pow(10, seeddigits) - 1)}".zfill(seeddigits)
|
||||
|
||||
|
||||
def main(args=None) -> tuple[argparse.Namespace, int]:
|
||||
def main(args=None) -> Tuple[argparse.Namespace, int]:
|
||||
# __name__ == "__main__" check so unittests that already imported worlds don't trip this.
|
||||
if __name__ == "__main__" and "worlds" in sys.modules:
|
||||
raise Exception("Worlds system should not be loaded before logging init.")
|
||||
@@ -95,7 +95,7 @@ def main(args=None) -> tuple[argparse.Namespace, int]:
|
||||
logging.info("Race mode enabled. Using non-deterministic random source.")
|
||||
random.seed() # reset to time-based random source
|
||||
|
||||
weights_cache: dict[str, tuple[Any, ...]] = {}
|
||||
weights_cache: Dict[str, Tuple[Any, ...]] = {}
|
||||
if args.weights_file_path and os.path.exists(args.weights_file_path):
|
||||
try:
|
||||
weights_cache[args.weights_file_path] = read_weights_yamls(args.weights_file_path)
|
||||
@@ -180,7 +180,7 @@ def main(args=None) -> tuple[argparse.Namespace, int]:
|
||||
erargs.name = {}
|
||||
erargs.csv_output = args.csv_output
|
||||
|
||||
settings_cache: dict[str, tuple[argparse.Namespace, ...]] = \
|
||||
settings_cache: Dict[str, Tuple[argparse.Namespace, ...]] = \
|
||||
{fname: (tuple(roll_settings(yaml, args.plando) for yaml in yamls) if args.sameoptions else None)
|
||||
for fname, yamls in weights_cache.items()}
|
||||
|
||||
@@ -212,7 +212,7 @@ def main(args=None) -> tuple[argparse.Namespace, int]:
|
||||
path = player_path_cache[player]
|
||||
if path:
|
||||
try:
|
||||
settings: tuple[argparse.Namespace, ...] = settings_cache[path] if settings_cache[path] else \
|
||||
settings: Tuple[argparse.Namespace, ...] = settings_cache[path] if settings_cache[path] else \
|
||||
tuple(roll_settings(yaml, args.plando) for yaml in weights_cache[path])
|
||||
for settingsObject in settings:
|
||||
for k, v in vars(settingsObject).items():
|
||||
@@ -242,7 +242,7 @@ def main(args=None) -> tuple[argparse.Namespace, int]:
|
||||
return erargs, seed
|
||||
|
||||
|
||||
def read_weights_yamls(path) -> tuple[Any, ...]:
|
||||
def read_weights_yamls(path) -> Tuple[Any, ...]:
|
||||
try:
|
||||
if urllib.parse.urlparse(path).scheme in ('https', 'file'):
|
||||
yaml = str(urllib.request.urlopen(path).read(), "utf-8-sig")
|
||||
@@ -252,20 +252,7 @@ def read_weights_yamls(path) -> tuple[Any, ...]:
|
||||
except Exception as e:
|
||||
raise Exception(f"Failed to read weights ({path})") from e
|
||||
|
||||
from yaml.error import MarkedYAMLError
|
||||
try:
|
||||
return tuple(parse_yamls(yaml))
|
||||
except MarkedYAMLError as ex:
|
||||
if ex.problem_mark:
|
||||
lines = yaml.splitlines()
|
||||
if ex.context_mark:
|
||||
relevant_lines = "\n".join(lines[ex.context_mark.line:ex.problem_mark.line+1])
|
||||
else:
|
||||
relevant_lines = lines[ex.problem_mark.line]
|
||||
error_line = " " * ex.problem_mark.column + "^"
|
||||
raise Exception(f"{ex.context} {ex.problem} on line {ex.problem_mark.line}:"
|
||||
f"\n{relevant_lines}\n{error_line}")
|
||||
raise ex
|
||||
return tuple(parse_yamls(yaml))
|
||||
|
||||
|
||||
def interpret_on_off(value) -> bool:
|
||||
@@ -334,6 +321,12 @@ def handle_name(name: str, player: int, name_counter: Counter):
|
||||
return new_name
|
||||
|
||||
|
||||
def roll_percentage(percentage: Union[int, float]) -> bool:
|
||||
"""Roll a percentage chance.
|
||||
percentage is expected to be in range [0, 100]"""
|
||||
return random.random() < (float(percentage) / 100)
|
||||
|
||||
|
||||
def update_weights(weights: dict, new_weights: dict, update_type: str, name: str) -> dict:
|
||||
logging.debug(f'Applying {new_weights}')
|
||||
cleaned_weights = {}
|
||||
@@ -378,7 +371,7 @@ def update_weights(weights: dict, new_weights: dict, update_type: str, name: str
|
||||
return weights
|
||||
|
||||
|
||||
def roll_meta_option(option_key, game: str, category_dict: dict) -> Any:
|
||||
def roll_meta_option(option_key, game: str, category_dict: Dict) -> Any:
|
||||
from worlds import AutoWorldRegister
|
||||
|
||||
if not game:
|
||||
@@ -399,7 +392,7 @@ def roll_linked_options(weights: dict) -> dict:
|
||||
if "name" not in option_set:
|
||||
raise ValueError("One of your linked options does not have a name.")
|
||||
try:
|
||||
if Options.roll_percentage(option_set["percentage"]):
|
||||
if roll_percentage(option_set["percentage"]):
|
||||
logging.debug(f"Linked option {option_set['name']} triggered.")
|
||||
new_options = option_set["options"]
|
||||
for category_name, category_options in new_options.items():
|
||||
@@ -432,7 +425,7 @@ def roll_triggers(weights: dict, triggers: list, valid_keys: set) -> dict:
|
||||
trigger_result = get_choice("option_result", option_set)
|
||||
result = get_choice(key, currently_targeted_weights)
|
||||
currently_targeted_weights[key] = result
|
||||
if result == trigger_result and Options.roll_percentage(get_choice("percentage", option_set, 100)):
|
||||
if result == trigger_result and roll_percentage(get_choice("percentage", option_set, 100)):
|
||||
for category_name, category_options in option_set["options"].items():
|
||||
currently_targeted_weights = weights
|
||||
if category_name:
|
||||
@@ -536,6 +529,10 @@ def roll_settings(weights: dict, plando_options: PlandoOptions = PlandoOptions.b
|
||||
handle_option(ret, game_weights, option_key, option, plando_options)
|
||||
valid_keys.add(option_key)
|
||||
|
||||
# TODO remove plando_items after moving it to the options system
|
||||
valid_keys.add("plando_items")
|
||||
if PlandoOptions.items in plando_options:
|
||||
ret.plando_items = copy.deepcopy(game_weights.get("plando_items", []))
|
||||
if ret.game == "A Link to the Past":
|
||||
# TODO there are still more LTTP options not on the options system
|
||||
valid_keys |= {"sprite_pool", "sprite", "random_sprite_on_event"}
|
||||
|
||||
21
Launcher.py
21
Launcher.py
@@ -16,10 +16,9 @@ import subprocess
|
||||
import sys
|
||||
import urllib.parse
|
||||
import webbrowser
|
||||
from collections.abc import Callable, Sequence
|
||||
from os.path import isfile
|
||||
from shutil import which
|
||||
from typing import Any
|
||||
from typing import Callable, Optional, Sequence, Tuple, Union, Any
|
||||
|
||||
if __name__ == "__main__":
|
||||
import ModuleUpdate
|
||||
@@ -85,16 +84,12 @@ def browse_files():
|
||||
def open_folder(folder_path):
|
||||
if is_linux:
|
||||
exe = which('xdg-open') or which('gnome-open') or which('kde-open')
|
||||
subprocess.Popen([exe, folder_path])
|
||||
elif is_macos:
|
||||
exe = which("open")
|
||||
else:
|
||||
webbrowser.open(folder_path)
|
||||
return
|
||||
|
||||
if exe:
|
||||
subprocess.Popen([exe, folder_path])
|
||||
else:
|
||||
logging.warning(f"No file browser available to open {folder_path}")
|
||||
webbrowser.open(folder_path)
|
||||
|
||||
|
||||
def update_settings():
|
||||
@@ -115,7 +110,7 @@ components.extend([
|
||||
])
|
||||
|
||||
|
||||
def handle_uri(path: str, launch_args: tuple[str, ...]) -> None:
|
||||
def handle_uri(path: str, launch_args: Tuple[str, ...]) -> None:
|
||||
url = urllib.parse.urlparse(path)
|
||||
queries = urllib.parse.parse_qs(url.query)
|
||||
launch_args = (path, *launch_args)
|
||||
@@ -163,7 +158,7 @@ def handle_uri(path: str, launch_args: tuple[str, ...]) -> None:
|
||||
).open()
|
||||
|
||||
|
||||
def identify(path: None | str) -> tuple[None | str, None | Component]:
|
||||
def identify(path: Union[None, str]) -> Tuple[Union[None, str], Union[None, Component]]:
|
||||
if path is None:
|
||||
return None, None
|
||||
for component in components:
|
||||
@@ -174,7 +169,7 @@ def identify(path: None | str) -> tuple[None | str, None | Component]:
|
||||
return None, None
|
||||
|
||||
|
||||
def get_exe(component: str | Component) -> Sequence[str] | None:
|
||||
def get_exe(component: Union[str, Component]) -> Optional[Sequence[str]]:
|
||||
if isinstance(component, str):
|
||||
name = component
|
||||
component = None
|
||||
@@ -227,7 +222,7 @@ def create_shortcut(button: Any, component: Component) -> None:
|
||||
button.menu.dismiss()
|
||||
|
||||
|
||||
refresh_components: Callable[[], None] | None = None
|
||||
refresh_components: Optional[Callable[[], None]] = None
|
||||
|
||||
|
||||
def run_gui(path: str, args: Any) -> None:
|
||||
@@ -452,7 +447,7 @@ def run_component(component: Component, *args):
|
||||
logging.warning(f"Component {component} does not appear to be executable.")
|
||||
|
||||
|
||||
def main(args: argparse.Namespace | dict | None = None):
|
||||
def main(args: Optional[Union[argparse.Namespace, dict]] = None):
|
||||
if isinstance(args, argparse.Namespace):
|
||||
args = {k: v for k, v in args._get_kwargs()}
|
||||
elif not args:
|
||||
|
||||
@@ -33,7 +33,7 @@ from worlds.ladx.TrackerConsts import storage_key
|
||||
from worlds.ladx.ItemTracker import ItemTracker
|
||||
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, Check
|
||||
from worlds.ladx.Tracker import LocationTracker, MagpieBridge
|
||||
|
||||
|
||||
class GameboyException(Exception):
|
||||
@@ -52,6 +52,22 @@ class BadRetroArchResponse(GameboyException):
|
||||
pass
|
||||
|
||||
|
||||
def magpie_logo():
|
||||
from kivy.uix.image import CoreImage
|
||||
binary_data = """
|
||||
iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAAAXN
|
||||
SR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA
|
||||
7DAcdvqGQAAADGSURBVDhPhVLBEcIwDHOYhjHCBuXHj2OTbAL8+
|
||||
MEGZIxOQ1CinOOk0Op0bmo7tlXXeR9FJMYDLOD9mwcLjQK7+hSZ
|
||||
wgcWMZJOAGeGKtChNHFL0j+FZD3jSCuo0w7l03wDrWdg00C4/aW
|
||||
eDEYNenuzPOfPspBnxf0kssE80vN0L8361j10P03DK4x6FHabuV
|
||||
ear8fHme+b17rwSjbAXeUMLb+EVTV2QHm46MWQanmnydA98KsVS
|
||||
XkV+qFpGQXrLhT/fqraQeQLuplpNH5g+WkAAAAASUVORK5CYII="""
|
||||
binary_data = base64.b64decode(binary_data)
|
||||
data = io.BytesIO(binary_data)
|
||||
return CoreImage(data, ext="png").texture
|
||||
|
||||
|
||||
class LAClientConstants:
|
||||
# Connector version
|
||||
VERSION = 0x01
|
||||
@@ -514,9 +530,7 @@ class LinksAwakeningContext(CommonContext):
|
||||
|
||||
def run_gui(self) -> None:
|
||||
import webbrowser
|
||||
from kvui import GameManager
|
||||
from kivy.metrics import dp
|
||||
from kivymd.uix.button import MDButton, MDButtonText
|
||||
from kvui import GameManager, ImageButton
|
||||
|
||||
class LADXManager(GameManager):
|
||||
logging_pairs = [
|
||||
@@ -529,10 +543,8 @@ class LinksAwakeningContext(CommonContext):
|
||||
b = super().build()
|
||||
|
||||
if self.ctx.magpie_enabled:
|
||||
button = MDButton(MDButtonText(text="Open Tracker"), style="filled", size=(dp(100), dp(70)), radius=5,
|
||||
size_hint_x=None, size_hint_y=None, pos_hint={"center_y": 0.55},
|
||||
on_press=lambda _: webbrowser.open('https://magpietracker.us/?enable_autotracker=1'))
|
||||
button.height = self.server_connect_bar.height
|
||||
button = ImageButton(texture=magpie_logo(), fit_mode="cover", image_size=(32, 32), size_hint_x=None,
|
||||
on_press=lambda _: webbrowser.open('https://magpietracker.us/?enable_autotracker=1'))
|
||||
self.connect_layout.add_widget(button)
|
||||
|
||||
return b
|
||||
@@ -626,11 +638,6 @@ class LinksAwakeningContext(CommonContext):
|
||||
"password": self.password,
|
||||
})
|
||||
|
||||
# We can process linked items on already-checked checks now that we have slot_data
|
||||
if self.client.tracker:
|
||||
checked_checks = set(self.client.tracker.all_checks) - set(self.client.tracker.remaining_checks)
|
||||
self.add_linked_items(checked_checks)
|
||||
|
||||
# TODO - use watcher_event
|
||||
if cmd == "ReceivedItems":
|
||||
for index, item in enumerate(args["items"], start=args["index"]):
|
||||
@@ -646,13 +653,6 @@ class LinksAwakeningContext(CommonContext):
|
||||
sync_msg = [{'cmd': 'Sync'}]
|
||||
await self.send_msgs(sync_msg)
|
||||
|
||||
def add_linked_items(self, checks: typing.List[Check]):
|
||||
for check in checks:
|
||||
if check.value and check.linkedItem:
|
||||
linkedItem = check.linkedItem
|
||||
if 'condition' not in linkedItem or (self.slot_data and linkedItem['condition'](self.slot_data)):
|
||||
self.client.item_tracker.setExtraItem(check.linkedItem['item'], check.linkedItem['qty'])
|
||||
|
||||
item_id_lookup = get_locations_to_id()
|
||||
|
||||
async def run_game_loop(self):
|
||||
@@ -661,7 +661,11 @@ class LinksAwakeningContext(CommonContext):
|
||||
checkMetadataTable[check.id])] for check in ladxr_checks]
|
||||
self.new_checks(checks, [check.id for check in ladxr_checks])
|
||||
|
||||
self.add_linked_items(ladxr_checks)
|
||||
for check in ladxr_checks:
|
||||
if check.value and check.linkedItem:
|
||||
linkedItem = check.linkedItem
|
||||
if 'condition' not in linkedItem or linkedItem['condition'](self.slot_data):
|
||||
self.client.item_tracker.setExtraItem(check.linkedItem['item'], check.linkedItem['qty'])
|
||||
|
||||
async def victory():
|
||||
await self.send_victory()
|
||||
|
||||
36
Main.py
36
Main.py
@@ -7,13 +7,14 @@ import tempfile
|
||||
import time
|
||||
import zipfile
|
||||
import zlib
|
||||
from typing import Dict, List, Optional, Set, Tuple, Union
|
||||
|
||||
import worlds
|
||||
from BaseClasses import CollectionState, Item, Location, LocationProgressType, MultiWorld
|
||||
from Fill import FillError, balance_multiworld_progression, distribute_items_restrictive, flood_items, \
|
||||
parse_planned_blocks, distribute_planned_blocks, resolve_early_locations_for_planned
|
||||
from BaseClasses import CollectionState, Item, Location, LocationProgressType, MultiWorld, Region
|
||||
from Fill import FillError, balance_multiworld_progression, distribute_items_restrictive, distribute_planned, \
|
||||
flood_items
|
||||
from Options import StartInventoryPool
|
||||
from Utils import __version__, output_path, version_tuple
|
||||
from Utils import __version__, output_path, version_tuple, get_settings
|
||||
from settings import get_settings
|
||||
from worlds import AutoWorld
|
||||
from worlds.generic.Rules import exclusion_rules, locality_rules
|
||||
@@ -21,7 +22,7 @@ from worlds.generic.Rules import exclusion_rules, locality_rules
|
||||
__all__ = ["main"]
|
||||
|
||||
|
||||
def main(args, seed=None, baked_server_options: dict[str, object] | None = None):
|
||||
def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = None):
|
||||
if not baked_server_options:
|
||||
baked_server_options = get_settings().server_options.as_dict()
|
||||
assert isinstance(baked_server_options, dict)
|
||||
@@ -36,6 +37,9 @@ def main(args, seed=None, baked_server_options: dict[str, object] | None = None)
|
||||
logger = logging.getLogger()
|
||||
multiworld.set_seed(seed, args.race, str(args.outputname) if args.outputname else None)
|
||||
multiworld.plando_options = args.plando_options
|
||||
multiworld.plando_items = args.plando_items.copy()
|
||||
multiworld.plando_texts = args.plando_texts.copy()
|
||||
multiworld.plando_connections = args.plando_connections.copy()
|
||||
multiworld.game = args.game.copy()
|
||||
multiworld.player_name = args.name.copy()
|
||||
multiworld.sprite = args.sprite.copy()
|
||||
@@ -131,15 +135,13 @@ def main(args, seed=None, baked_server_options: dict[str, object] | None = None)
|
||||
multiworld.worlds[1].options.non_local_items.value = set()
|
||||
multiworld.worlds[1].options.local_items.value = set()
|
||||
|
||||
multiworld.plando_item_blocks = parse_planned_blocks(multiworld)
|
||||
|
||||
AutoWorld.call_all(multiworld, "connect_entrances")
|
||||
AutoWorld.call_all(multiworld, "generate_basic")
|
||||
|
||||
# remove starting inventory from pool items.
|
||||
# Because some worlds don't actually create items during create_items this has to be as late as possible.
|
||||
fallback_inventory = StartInventoryPool({})
|
||||
depletion_pool: dict[int, dict[str, int]] = {
|
||||
depletion_pool: Dict[int, Dict[str, int]] = {
|
||||
player: getattr(multiworld.worlds[player].options, "start_inventory_from_pool", fallback_inventory).value.copy()
|
||||
for player in multiworld.player_ids
|
||||
}
|
||||
@@ -148,7 +150,7 @@ def main(args, seed=None, baked_server_options: dict[str, object] | None = None)
|
||||
}
|
||||
|
||||
if target_per_player:
|
||||
new_itempool: list[Item] = []
|
||||
new_itempool: List[Item] = []
|
||||
|
||||
# Make new itempool with start_inventory_from_pool items removed
|
||||
for item in multiworld.itempool:
|
||||
@@ -177,9 +179,8 @@ def main(args, seed=None, baked_server_options: dict[str, object] | None = None)
|
||||
multiworld._all_state = None
|
||||
|
||||
logger.info("Running Item Plando.")
|
||||
resolve_early_locations_for_planned(multiworld)
|
||||
distribute_planned_blocks(multiworld, [x for player in multiworld.plando_item_blocks
|
||||
for x in multiworld.plando_item_blocks[player]])
|
||||
|
||||
distribute_planned(multiworld)
|
||||
|
||||
logger.info('Running Pre Main Fill.')
|
||||
|
||||
@@ -232,7 +233,7 @@ def main(args, seed=None, baked_server_options: dict[str, object] | None = None)
|
||||
pool.submit(AutoWorld.call_single, multiworld, "generate_output", player, temp_dir))
|
||||
|
||||
# collect ER hint info
|
||||
er_hint_data: dict[int, dict[int, str]] = {}
|
||||
er_hint_data: Dict[int, Dict[int, str]] = {}
|
||||
AutoWorld.call_all(multiworld, 'extend_hint_information', er_hint_data)
|
||||
|
||||
def write_multidata():
|
||||
@@ -273,7 +274,7 @@ def main(args, seed=None, baked_server_options: dict[str, object] | None = None)
|
||||
for player in multiworld.groups[location.item.player]["players"]:
|
||||
precollected_hints[player].add(hint)
|
||||
|
||||
locations_data: dict[int, dict[int, tuple[int, int, int]]] = {player: {} for player in multiworld.player_ids}
|
||||
locations_data: Dict[int, Dict[int, Tuple[int, int, int]]] = {player: {} for player in multiworld.player_ids}
|
||||
for location in multiworld.get_filled_locations():
|
||||
if type(location.address) == int:
|
||||
assert location.item.code is not None, "item code None should be event, " \
|
||||
@@ -300,14 +301,13 @@ def main(args, seed=None, baked_server_options: dict[str, object] | None = None)
|
||||
game_world.game: worlds.network_data_package["games"][game_world.game]
|
||||
for game_world in multiworld.worlds.values()
|
||||
}
|
||||
data_package["Archipelago"] = worlds.network_data_package["games"]["Archipelago"]
|
||||
|
||||
checks_in_area: dict[int, dict[str, int | list[int]]] = {}
|
||||
checks_in_area: Dict[int, Dict[str, Union[int, List[int]]]] = {}
|
||||
|
||||
# get spheres -> filter address==None -> skip empty
|
||||
spheres: list[dict[int, set[int]]] = []
|
||||
spheres: List[Dict[int, Set[int]]] = []
|
||||
for sphere in multiworld.get_sendable_spheres():
|
||||
current_sphere: dict[int, set[int]] = collections.defaultdict(set)
|
||||
current_sphere: Dict[int, Set[int]] = collections.defaultdict(set)
|
||||
for sphere_location in sphere:
|
||||
current_sphere[sphere_location.player].add(sphere_location.address)
|
||||
|
||||
|
||||
@@ -1826,7 +1826,7 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
|
||||
ctx.clients[team][slot].append(client)
|
||||
client.version = args['version']
|
||||
client.tags = args['tags']
|
||||
client.no_locations = bool(client.tags & _non_game_messages.keys())
|
||||
client.no_locations = "TextOnly" in client.tags or "Tracker" in client.tags
|
||||
# set NoText for old PopTracker clients that predate the tag to save traffic
|
||||
client.no_text = "NoText" in client.tags or ("PopTracker" in client.tags and client.version < (0, 5, 1))
|
||||
connected_packet = {
|
||||
@@ -1900,7 +1900,7 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
|
||||
old_tags = client.tags
|
||||
client.tags = args["tags"]
|
||||
if set(old_tags) != set(client.tags):
|
||||
client.no_locations = bool(client.tags & _non_game_messages.keys())
|
||||
client.no_locations = 'TextOnly' in client.tags or 'Tracker' in client.tags
|
||||
client.no_text = "NoText" in client.tags or (
|
||||
"PopTracker" in client.tags and client.version < (0, 5, 1)
|
||||
)
|
||||
@@ -1990,14 +1990,9 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
|
||||
ctx.save()
|
||||
for slot in concerning_slots:
|
||||
ctx.on_changed_hints(client.team, slot)
|
||||
|
||||
|
||||
elif cmd == 'StatusUpdate':
|
||||
if client.no_locations and args["status"] == ClientStatus.CLIENT_GOAL:
|
||||
await ctx.send_msgs(client, [{'cmd': 'InvalidPacket', "type": "cmd",
|
||||
"text": "Trackers can't register Goal Complete",
|
||||
"original_cmd": cmd}])
|
||||
else:
|
||||
update_client_status(ctx, client, args["status"])
|
||||
update_client_status(ctx, client, args["status"])
|
||||
|
||||
elif cmd == 'Say':
|
||||
if "text" not in args or type(args["text"]) is not str or not args["text"].isprintable():
|
||||
@@ -2368,6 +2363,7 @@ class ServerCommandProcessor(CommonCommandProcessor):
|
||||
known_options = (f"{option}: {option_type}" for option, option_type in self.ctx.simple_options.items())
|
||||
self.output(f"Unrecognized option '{option_name}', known: {', '.join(known_options)}")
|
||||
return False
|
||||
|
||||
if value_type == bool:
|
||||
def value_type(input_text: str):
|
||||
return input_text.lower() not in {"off", "0", "false", "none", "null", "no"}
|
||||
@@ -2401,75 +2397,6 @@ class ServerCommandProcessor(CommonCommandProcessor):
|
||||
f"approximately totaling {Utils.format_SI_prefix(total, power=1024)}B")
|
||||
self.output("\n".join(texts))
|
||||
|
||||
def _cmd_discord_webhook(self, webhook_url: str):
|
||||
"""Needs to be supplied with a Discord WebHook url as parameter,
|
||||
which will then relay the server log to a discord channel."""
|
||||
|
||||
import discord_webhook
|
||||
initial_response = discord_webhook.DiscordWebhook(webhook_url, wait=True,
|
||||
content="Beginning Discord Logging").execute()
|
||||
if initial_response.ok:
|
||||
import queue
|
||||
response_queue = queue.SimpleQueue()
|
||||
|
||||
class Emitter(threading.Thread):
|
||||
def run(self):
|
||||
record: typing.Optional[logging.LogRecord] = None
|
||||
while True:
|
||||
time.sleep(1)
|
||||
# check for leftover record from last iteration
|
||||
message = record.msg if record else ""
|
||||
while 1:
|
||||
try:
|
||||
record = response_queue.get_nowait()
|
||||
except queue.Empty:
|
||||
break
|
||||
else:
|
||||
if record is None:
|
||||
return # shutdown
|
||||
if len(record.msg) > 1999:
|
||||
continue # content size limit
|
||||
if len(message) + len(record.msg) > 2000:
|
||||
break # reached content size limit in total
|
||||
else:
|
||||
message += "\n" + record.msg
|
||||
record = None
|
||||
if message:
|
||||
try:
|
||||
response = discord_webhook.DiscordWebhook(
|
||||
webhook_url, rate_limit_retry=True, content=message.strip()).execute()
|
||||
if response.status_code not in (200, 204):
|
||||
shutdown()
|
||||
logging.info(f"Disabled Discord WebHook due to error code {response.status_code}.")
|
||||
return
|
||||
# just in case to prevent an error-loop logging itself
|
||||
except Exception as e:
|
||||
shutdown()
|
||||
logging.error("Disabled Discord WebHook due to error.")
|
||||
logging.exception(e)
|
||||
return
|
||||
|
||||
emitter = Emitter()
|
||||
emitter.daemon = True
|
||||
emitter.start()
|
||||
|
||||
class DiscordLogger(logging.Handler):
|
||||
"""Logs to Discord WebHook"""
|
||||
def emit(self, record: logging.LogRecord):
|
||||
response_queue.put(record)
|
||||
|
||||
handler = DiscordLogger()
|
||||
|
||||
def shutdown():
|
||||
response_queue.put(None)
|
||||
logging.getLogger().removeHandler(handler)
|
||||
|
||||
logging.getLogger().addHandler(handler)
|
||||
self.output("Discord Link established.")
|
||||
|
||||
else:
|
||||
self.output("Discord Link could not be established. Check your webhook url.")
|
||||
|
||||
|
||||
async def console(ctx: Context):
|
||||
import sys
|
||||
|
||||
202
Options.py
202
Options.py
@@ -24,12 +24,6 @@ if typing.TYPE_CHECKING:
|
||||
import pathlib
|
||||
|
||||
|
||||
def roll_percentage(percentage: int | float) -> bool:
|
||||
"""Roll a percentage chance.
|
||||
percentage is expected to be in range [0, 100]"""
|
||||
return random.random() < (float(percentage) / 100)
|
||||
|
||||
|
||||
class OptionError(ValueError):
|
||||
pass
|
||||
|
||||
@@ -1025,7 +1019,7 @@ class PlandoTexts(Option[typing.List[PlandoText]], VerifyKeys):
|
||||
if isinstance(data, typing.Iterable):
|
||||
for text in data:
|
||||
if isinstance(text, typing.Mapping):
|
||||
if roll_percentage(text.get("percentage", 100)):
|
||||
if random.random() < float(text.get("percentage", 100)/100):
|
||||
at = text.get("at", None)
|
||||
if at is not None:
|
||||
if isinstance(at, dict):
|
||||
@@ -1051,7 +1045,7 @@ class PlandoTexts(Option[typing.List[PlandoText]], VerifyKeys):
|
||||
else:
|
||||
raise OptionError("\"at\" must be a valid string or weighted list of strings!")
|
||||
elif isinstance(text, PlandoText):
|
||||
if roll_percentage(text.percentage):
|
||||
if random.random() < float(text.percentage/100):
|
||||
texts.append(text)
|
||||
else:
|
||||
raise Exception(f"Cannot create plando text from non-dictionary type, got {type(text)}")
|
||||
@@ -1175,7 +1169,7 @@ class PlandoConnections(Option[typing.List[PlandoConnection]], metaclass=Connect
|
||||
for connection in data:
|
||||
if isinstance(connection, typing.Mapping):
|
||||
percentage = connection.get("percentage", 100)
|
||||
if roll_percentage(percentage):
|
||||
if random.random() < float(percentage / 100):
|
||||
entrance = connection.get("entrance", None)
|
||||
if is_iterable_except_str(entrance):
|
||||
entrance = random.choice(sorted(entrance))
|
||||
@@ -1193,7 +1187,7 @@ class PlandoConnections(Option[typing.List[PlandoConnection]], metaclass=Connect
|
||||
percentage
|
||||
))
|
||||
elif isinstance(connection, PlandoConnection):
|
||||
if roll_percentage(connection.percentage):
|
||||
if random.random() < float(connection.percentage / 100):
|
||||
value.append(connection)
|
||||
else:
|
||||
raise Exception(f"Cannot create connection from non-Dict type, got {type(connection)}.")
|
||||
@@ -1298,47 +1292,42 @@ class CommonOptions(metaclass=OptionsMetaProperty):
|
||||
progression_balancing: ProgressionBalancing
|
||||
accessibility: Accessibility
|
||||
|
||||
def as_dict(
|
||||
self,
|
||||
*option_names: str,
|
||||
casing: typing.Literal["snake", "camel", "pascal", "kebab"] = "snake",
|
||||
toggles_as_bools: bool = False,
|
||||
) -> dict[str, typing.Any]:
|
||||
def as_dict(self,
|
||||
*option_names: str,
|
||||
casing: typing.Literal["snake", "camel", "pascal", "kebab"] = "snake",
|
||||
toggles_as_bools: bool = False) -> typing.Dict[str, typing.Any]:
|
||||
"""
|
||||
Returns a dictionary of [str, Option.value]
|
||||
|
||||
:param option_names: Names of the options to get the values of.
|
||||
:param casing: Casing of the keys to return. Supports `snake`, `camel`, `pascal`, `kebab`.
|
||||
:param toggles_as_bools: Whether toggle options should be returned as bools instead of ints.
|
||||
|
||||
:return: A dictionary of each option name to the value of its Option. If the option is an OptionSet, the value
|
||||
will be returned as a sorted list.
|
||||
:param option_names: names of the options to return
|
||||
:param casing: case of the keys to return. Supports `snake`, `camel`, `pascal`, `kebab`
|
||||
:param toggles_as_bools: whether toggle options should be output as bools instead of strings
|
||||
"""
|
||||
assert option_names, "options.as_dict() was used without any option names."
|
||||
option_results = {}
|
||||
for option_name in option_names:
|
||||
if option_name not in type(self).type_hints:
|
||||
raise ValueError(f"{option_name} not found in {tuple(type(self).type_hints)}")
|
||||
|
||||
if casing == "snake":
|
||||
display_name = option_name
|
||||
elif casing == "camel":
|
||||
split_name = [name.title() for name in option_name.split("_")]
|
||||
split_name[0] = split_name[0].lower()
|
||||
display_name = "".join(split_name)
|
||||
elif casing == "pascal":
|
||||
display_name = "".join([name.title() for name in option_name.split("_")])
|
||||
elif casing == "kebab":
|
||||
display_name = option_name.replace("_", "-")
|
||||
if option_name in type(self).type_hints:
|
||||
if casing == "snake":
|
||||
display_name = option_name
|
||||
elif casing == "camel":
|
||||
split_name = [name.title() for name in option_name.split("_")]
|
||||
split_name[0] = split_name[0].lower()
|
||||
display_name = "".join(split_name)
|
||||
elif casing == "pascal":
|
||||
display_name = "".join([name.title() for name in option_name.split("_")])
|
||||
elif casing == "kebab":
|
||||
display_name = option_name.replace("_", "-")
|
||||
else:
|
||||
raise ValueError(f"{casing} is invalid casing for as_dict. "
|
||||
"Valid names are 'snake', 'camel', 'pascal', 'kebab'.")
|
||||
value = getattr(self, option_name).value
|
||||
if isinstance(value, set):
|
||||
value = sorted(value)
|
||||
elif toggles_as_bools and issubclass(type(self).type_hints[option_name], Toggle):
|
||||
value = bool(value)
|
||||
option_results[display_name] = value
|
||||
else:
|
||||
raise ValueError(f"{casing} is invalid casing for as_dict. "
|
||||
"Valid names are 'snake', 'camel', 'pascal', 'kebab'.")
|
||||
value = getattr(self, option_name).value
|
||||
if isinstance(value, set):
|
||||
value = sorted(value)
|
||||
elif toggles_as_bools and issubclass(type(self).type_hints[option_name], Toggle):
|
||||
value = bool(value)
|
||||
option_results[display_name] = value
|
||||
raise ValueError(f"{option_name} not found in {tuple(type(self).type_hints)}")
|
||||
return option_results
|
||||
|
||||
|
||||
@@ -1359,7 +1348,6 @@ class StartInventory(ItemDict):
|
||||
verify_item_name = True
|
||||
display_name = "Start Inventory"
|
||||
rich_text_doc = True
|
||||
max = 10000
|
||||
|
||||
|
||||
class StartInventoryPool(StartInventory):
|
||||
@@ -1475,131 +1463,6 @@ class ItemLinks(OptionList):
|
||||
link["item_pool"] = list(pool)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PlandoItem:
|
||||
items: list[str] | dict[str, typing.Any]
|
||||
locations: list[str]
|
||||
world: int | str | bool | None | typing.Iterable[str] | set[int] = False
|
||||
from_pool: bool = True
|
||||
force: bool | typing.Literal["silent"] = "silent"
|
||||
count: int | bool | dict[str, int] = False
|
||||
percentage: int = 100
|
||||
|
||||
|
||||
class PlandoItems(Option[typing.List[PlandoItem]]):
|
||||
"""Generic items plando."""
|
||||
default = ()
|
||||
supports_weighting = False
|
||||
display_name = "Plando Items"
|
||||
|
||||
def __init__(self, value: typing.Iterable[PlandoItem]) -> None:
|
||||
self.value = list(deepcopy(value))
|
||||
super().__init__()
|
||||
|
||||
@classmethod
|
||||
def from_any(cls, data: typing.Any) -> Option[typing.List[PlandoItem]]:
|
||||
if not isinstance(data, typing.Iterable):
|
||||
raise OptionError(f"Cannot create plando items from non-Iterable type, got {type(data)}")
|
||||
|
||||
value: typing.List[PlandoItem] = []
|
||||
for item in data:
|
||||
if isinstance(item, typing.Mapping):
|
||||
percentage = item.get("percentage", 100)
|
||||
if not isinstance(percentage, int):
|
||||
raise OptionError(f"Plando `percentage` has to be int, not {type(percentage)}.")
|
||||
if not (0 <= percentage <= 100):
|
||||
raise OptionError(f"Plando `percentage` has to be between 0 and 100 (inclusive) not {percentage}.")
|
||||
if roll_percentage(percentage):
|
||||
count = item.get("count", False)
|
||||
items = item.get("items", [])
|
||||
if not items:
|
||||
items = item.get("item", None) # explicitly throw an error here if not present
|
||||
if not items:
|
||||
raise OptionError("You must specify at least one item to place items with plando.")
|
||||
count = 1
|
||||
if isinstance(items, str):
|
||||
items = [items]
|
||||
elif not isinstance(items, (dict, list)):
|
||||
raise OptionError(f"Plando 'items' has to be string, list, or "
|
||||
f"dictionary, not {type(items)}")
|
||||
locations = item.get("locations", [])
|
||||
if not locations:
|
||||
locations = item.get("location", ["Everywhere"])
|
||||
if locations:
|
||||
count = 1
|
||||
if isinstance(locations, str):
|
||||
locations = [locations]
|
||||
if not isinstance(locations, list):
|
||||
raise OptionError(f"Plando `location` has to be string or list, not {type(locations)}")
|
||||
world = item.get("world", False)
|
||||
from_pool = item.get("from_pool", True)
|
||||
force = item.get("force", "silent")
|
||||
if not isinstance(from_pool, bool):
|
||||
raise OptionError(f"Plando 'from_pool' has to be true or false, not {from_pool!r}.")
|
||||
if not (isinstance(force, bool) or force == "silent"):
|
||||
raise OptionError(f"Plando `force` has to be true or false or `silent`, not {force!r}.")
|
||||
value.append(PlandoItem(items, locations, world, from_pool, force, count, percentage))
|
||||
elif isinstance(item, PlandoItem):
|
||||
if roll_percentage(item.percentage):
|
||||
value.append(item)
|
||||
else:
|
||||
raise OptionError(f"Cannot create plando item from non-Dict type, got {type(item)}.")
|
||||
return cls(value)
|
||||
|
||||
def verify(self, world: typing.Type[World], player_name: str, plando_options: "PlandoOptions") -> None:
|
||||
if not self.value:
|
||||
return
|
||||
from BaseClasses import PlandoOptions
|
||||
if not (PlandoOptions.items & plando_options):
|
||||
# plando is disabled but plando options were given so overwrite the options
|
||||
self.value = []
|
||||
logging.warning(f"The plando items module is turned off, "
|
||||
f"so items for {player_name} will be ignored.")
|
||||
else:
|
||||
# filter down item groups
|
||||
for plando in self.value:
|
||||
# confirm a valid count
|
||||
if isinstance(plando.count, dict):
|
||||
if "min" in plando.count and "max" in plando.count:
|
||||
if plando.count["min"] > plando.count["max"]:
|
||||
raise OptionError("Plando cannot have count `min` greater than `max`.")
|
||||
items_copy = plando.items.copy()
|
||||
if isinstance(plando.items, dict):
|
||||
for item in items_copy:
|
||||
if item in world.item_name_groups:
|
||||
value = plando.items.pop(item)
|
||||
group = world.item_name_groups[item]
|
||||
filtered_items = sorted(group.difference(list(plando.items.keys())))
|
||||
if not filtered_items:
|
||||
raise OptionError(f"Plando `items` contains the group \"{item}\" "
|
||||
f"and every item in it. This is not allowed.")
|
||||
if value is True:
|
||||
for key in filtered_items:
|
||||
plando.items[key] = True
|
||||
else:
|
||||
for key in random.choices(filtered_items, k=value):
|
||||
plando.items[key] = plando.items.get(key, 0) + 1
|
||||
else:
|
||||
assert isinstance(plando.items, list) # pycharm can't figure out the hinting without the hint
|
||||
for item in items_copy:
|
||||
if item in world.item_name_groups:
|
||||
plando.items.remove(item)
|
||||
plando.items.extend(sorted(world.item_name_groups[item]))
|
||||
|
||||
@classmethod
|
||||
def get_option_name(cls, value: list[PlandoItem]) -> str:
|
||||
return ", ".join(["(%s: %s)" % (item.items, item.locations) for item in value]) #TODO: see what a better way to display would be
|
||||
|
||||
def __getitem__(self, index: typing.SupportsIndex) -> PlandoItem:
|
||||
return self.value.__getitem__(index)
|
||||
|
||||
def __iter__(self) -> typing.Iterator[PlandoItem]:
|
||||
yield from self.value
|
||||
|
||||
def __len__(self) -> int:
|
||||
return len(self.value)
|
||||
|
||||
|
||||
class Removed(FreeText):
|
||||
"""This Option has been Removed."""
|
||||
rich_text_doc = True
|
||||
@@ -1622,7 +1485,6 @@ class PerGameCommonOptions(CommonOptions):
|
||||
exclude_locations: ExcludeLocations
|
||||
priority_locations: PriorityLocations
|
||||
item_links: ItemLinks
|
||||
plando_items: PlandoItems
|
||||
|
||||
|
||||
@dataclass
|
||||
|
||||
11
Utils.py
11
Utils.py
@@ -139,11 +139,8 @@ def local_path(*path: str) -> str:
|
||||
local_path.cached_path = os.path.dirname(os.path.abspath(sys.argv[0]))
|
||||
else:
|
||||
import __main__
|
||||
if globals().get("__file__") and os.path.isfile(__file__):
|
||||
if hasattr(__main__, "__file__") and os.path.isfile(__main__.__file__):
|
||||
# we are running in a normal Python environment
|
||||
local_path.cached_path = os.path.dirname(os.path.abspath(__file__))
|
||||
elif hasattr(__main__, "__file__") and os.path.isfile(__main__.__file__):
|
||||
# we are running in a normal Python environment, but AP was imported weirdly
|
||||
local_path.cached_path = os.path.dirname(os.path.abspath(__main__.__file__))
|
||||
else:
|
||||
# pray
|
||||
@@ -638,8 +635,6 @@ def get_fuzzy_results(input_word: str, word_list: typing.Collection[str], limit:
|
||||
import jellyfish
|
||||
|
||||
def get_fuzzy_ratio(word1: str, word2: str) -> float:
|
||||
if word1 == word2:
|
||||
return 1.01
|
||||
return (1 - jellyfish.damerau_levenshtein_distance(word1.lower(), word2.lower())
|
||||
/ max(len(word1), len(word2)))
|
||||
|
||||
@@ -660,10 +655,8 @@ def get_intended_text(input_text: str, possible_answers) -> typing.Tuple[str, bo
|
||||
picks = get_fuzzy_results(input_text, possible_answers, limit=2)
|
||||
if len(picks) > 1:
|
||||
dif = picks[0][1] - picks[1][1]
|
||||
if picks[0][1] == 101:
|
||||
if picks[0][1] == 100:
|
||||
return picks[0][0], True, "Perfect Match"
|
||||
elif picks[0][1] == 100:
|
||||
return picks[0][0], True, "Case Insensitive Perfect Match"
|
||||
elif picks[0][1] < 75:
|
||||
return picks[0][0], False, f"Didn't find something that closely matches '{input_text}', " \
|
||||
f"did you mean '{picks[0][0]}'? ({picks[0][1]}% sure)"
|
||||
|
||||
@@ -17,9 +17,7 @@
|
||||
This page allows you to host a game which was not generated by the website. For example, if you have
|
||||
generated a game on your own computer, you may upload the zip file created by the generator to
|
||||
host the game here. This will also provide a tracker, and the ability for your players to download
|
||||
their patch files if the game is core-verified. For Custom Games, you can find the patch files in
|
||||
the output .zip file you are uploading here. You need to manually distribute those patch files to
|
||||
your players.
|
||||
their patch files.
|
||||
</p>
|
||||
<p>In addition to the zip file created by the generator, you may upload a multidata file here as well.</p>
|
||||
<div id="host-game-form-wrapper">
|
||||
|
||||
BIN
data/sprites/ladx/Bowwow.bdiff
Normal file
BIN
data/sprites/ladx/Bowwow.bdiff
Normal file
Binary file not shown.
BIN
data/sprites/ladx/Bunny.bdiff
Normal file
BIN
data/sprites/ladx/Bunny.bdiff
Normal file
Binary file not shown.
BIN
data/sprites/ladx/Luigi.bdiff
Normal file
BIN
data/sprites/ladx/Luigi.bdiff
Normal file
Binary file not shown.
BIN
data/sprites/ladx/Mario.bdiff
Normal file
BIN
data/sprites/ladx/Mario.bdiff
Normal file
Binary file not shown.
BIN
data/sprites/ladx/Richard.bdiff
Normal file
BIN
data/sprites/ladx/Richard.bdiff
Normal file
Binary file not shown.
BIN
data/sprites/ladx/Tarin.bdiff
Normal file
BIN
data/sprites/ladx/Tarin.bdiff
Normal file
Binary file not shown.
@@ -8,11 +8,7 @@ including [Contributing](contributing.md), [Adding Games](<adding games.md>), an
|
||||
|
||||
### My game has a restrictive start that leads to fill errors
|
||||
|
||||
A "restrictive start" here means having a combination of very few sphere 1 locations and potentially requiring more
|
||||
than one item to get a player to sphere 2.
|
||||
|
||||
One way to fix this is to hint to the Generator that an item needs to be in sphere one with local_early_items.
|
||||
Here, `1` represents the number of "Sword" items the Generator will attempt to place in sphere one.
|
||||
Hint to the Generator that an item needs to be in sphere one with local_early_items. Here, `1` represents the number of "Sword" items to attempt to place in sphere one.
|
||||
```py
|
||||
early_item_name = "Sword"
|
||||
self.multiworld.local_early_items[self.player][early_item_name] = 1
|
||||
@@ -22,19 +18,15 @@ Some alternative ways to try to fix this problem are:
|
||||
* Add more locations to sphere one of your world, potentially only when there would be a restrictive start
|
||||
* Pre-place items yourself, such as during `create_items`
|
||||
* Put items into the player's starting inventory using `push_precollected`
|
||||
* Raise an exception, such as an `OptionError` during `generate_early`, to disallow options that would lead to a
|
||||
restrictive start
|
||||
* Raise an exception, such as an `OptionError` during `generate_early`, to disallow options that would lead to a restrictive start
|
||||
|
||||
---
|
||||
|
||||
### I have multiple options that change the item/location pool counts and need to make sure I am not submitting more/fewer items than locations
|
||||
### I have multiple settings that change the item/location pool counts and need to balance them out
|
||||
|
||||
In an ideal situation your system for producing locations and items wouldn't leave any opportunity for them to be
|
||||
unbalanced. But in real, complex situations, that might be unfeasible.
|
||||
In an ideal situation your system for producing locations and items wouldn't leave any opportunity for them to be unbalanced. But in real, complex situations, that might be unfeasible.
|
||||
|
||||
If that's the case, you can create extra filler based on the difference between your unfilled locations and your
|
||||
itempool by comparing [get_unfilled_locations](https://github.com/ArchipelagoMW/Archipelago/blob/main/BaseClasses.py#:~:text=get_unfilled_locations)
|
||||
to your list of items to submit
|
||||
If that's the case, you can create extra filler based on the difference between your unfilled locations and your itempool by comparing [get_unfilled_locations](https://github.com/ArchipelagoMW/Archipelago/blob/main/BaseClasses.py#:~:text=get_unfilled_locations) to your list of items to submit
|
||||
|
||||
Note: to use self.create_filler(), self.get_filler_item_name() should be defined to only return valid filler item names
|
||||
```py
|
||||
@@ -47,8 +39,7 @@ for _ in range(total_locations - len(item_pool)):
|
||||
self.multiworld.itempool += item_pool
|
||||
```
|
||||
|
||||
A faster alternative to the `for` loop would be to use a
|
||||
[list comprehension](https://docs.python.org/3/tutorial/datastructures.html#list-comprehensions):
|
||||
A faster alternative to the `for` loop would be to use a [list comprehension](https://docs.python.org/3/tutorial/datastructures.html#list-comprehensions):
|
||||
```py
|
||||
item_pool += [self.create_filler() for _ in range(total_locations - len(item_pool))]
|
||||
```
|
||||
@@ -57,39 +48,24 @@ item_pool += [self.create_filler() for _ in range(total_locations - len(item_poo
|
||||
|
||||
### I learned about indirect conditions in the world API document, but I want to know more. What are they and why are they necessary?
|
||||
|
||||
The world API document mentions how to use `multiworld.register_indirect_condition` to register indirect conditions and
|
||||
**when** you should use them, but not *how* they work and *why* they are necessary. This is because the explanation is
|
||||
quite complicated.
|
||||
The world API document mentions how to use `multiworld.register_indirect_condition` to register indirect conditions and **when** you should use them, but not *how* they work and *why* they are necessary. This is because the explanation is quite complicated.
|
||||
|
||||
Region sweep (the algorithm that determines which regions are reachable) is a Breadth-First Search of the region graph.
|
||||
It starts from the origin region, checks entrances one by one, and adds newly reached regions and their entrances to
|
||||
the queue until there is nothing more to check.
|
||||
Region sweep (the algorithm that determines which regions are reachable) is a Breadth-First Search of the region graph. It starts from the origin region, checks entrances one by one, and adds newly reached regions and their entrances to the queue until there is nothing more to check.
|
||||
|
||||
For performance reasons, AP only checks every entrance once. However, if an entrance's access_rule depends on region
|
||||
access, then the following may happen:
|
||||
1. The entrance is checked and determined to be nontraversable because the region in its access_rule hasn't been
|
||||
reached yet during the graph search.
|
||||
For performance reasons, AP only checks every entrance once. However, if an entrance's access_rule depends on region access, then the following may happen:
|
||||
1. The entrance is checked and determined to be nontraversable because the region in its access_rule hasn't been reached yet during the graph search.
|
||||
2. Then, the region in its access_rule is determined to be reachable.
|
||||
|
||||
This entrance *would* be in logic if it were rechecked, but it won't be rechecked this cycle.
|
||||
To account for this case, AP would have to recheck all entrances every time a new region is reached until no new
|
||||
regions are reached.
|
||||
To account for this case, AP would have to recheck all entrances every time a new region is reached until no new regions are reached.
|
||||
|
||||
An indirect condition is how you can manually define that a specific entrance needs to be rechecked during region sweep
|
||||
if a specific region is reached during it.
|
||||
This keeps most of the performance upsides. Even in a game making heavy use of indirect conditions (ex: The Witness),
|
||||
using them is significantly faster than just "rechecking each entrance until nothing new is found".
|
||||
The reason entrance access rules using `location.can_reach` and `entrance.can_reach` are also affected is because they
|
||||
call `region.can_reach` on their respective parent/source region.
|
||||
An indirect condition is how you can manually define that a specific entrance needs to be rechecked during region sweep if a specific region is reached during it.
|
||||
This keeps most of the performance upsides. Even in a game making heavy use of indirect conditions (ex: The Witness), using them is significantly faster than just "rechecking each entrance until nothing new is found".
|
||||
The reason entrance access rules using `location.can_reach` and `entrance.can_reach` are also affected is because they call `region.can_reach` on their respective parent/source region.
|
||||
|
||||
We recognize it can feel like a trap since it will not alert you when you are missing an indirect condition,
|
||||
and that some games have very complex access rules.
|
||||
As of [PR #3682 (Core: Region handling customization)](https://github.com/ArchipelagoMW/Archipelago/pull/3682)
|
||||
being merged, it is possible for a world to opt out of indirect conditions entirely, instead using the system of
|
||||
checking each entrance whenever a region has been reached, although this does come with a performance cost.
|
||||
Opting out of using indirect conditions should only be used by games that *really* need it. For most games, it should
|
||||
be reasonable to know all entrance → region dependencies, making indirect conditions preferred because they are
|
||||
much faster.
|
||||
We recognize it can feel like a trap since it will not alert you when you are missing an indirect condition, and that some games have very complex access rules.
|
||||
As of [PR #3682 (Core: Region handling customization)](https://github.com/ArchipelagoMW/Archipelago/pull/3682) being merged, it is possible for a world to opt out of indirect conditions entirely, instead using the system of checking each entrance whenever a region has been reached, although this does come with a performance cost.
|
||||
Opting out of using indirect conditions should only be used by games that *really* need it. For most games, it should be reasonable to know all entrance → region dependencies, making indirect conditions preferred because they are much faster.
|
||||
|
||||
---
|
||||
|
||||
@@ -109,34 +85,3 @@ Common situations where this can happen include:
|
||||
Also, consider using the `options.as_dict("option_name", "option_two")` helper.
|
||||
* Using enums as Location/Item names in the datapackage. When building out `location_name_to_id` and `item_name_to_id`,
|
||||
make sure that you are not using your enum class for either the names or ids in these mappings.
|
||||
|
||||
---
|
||||
|
||||
### Some locations are technically possible to check with few or no items, but they'd be very tedious or frustrating. How do worlds deal with this?
|
||||
|
||||
Sometimes the game can be modded to skip these locations or make them less tedious. But when this issue is due to a fundamental aspect of the game, then the general answer is "soft logic" (and its subtypes like "combat logic", "money logic", etc.). For example: you can logically require that a player have several helpful items before fighting the final boss, even if a skilled player technically needs no items to beat it. Randomizer logic should describe what's *fun* rather than what's technically possible.
|
||||
|
||||
Concrete examples of soft logic include:
|
||||
- Defeating a boss might logically require health upgrades, damage upgrades, certain weapons, etc. that aren't strictly necessary.
|
||||
- Entering a high-level area might logically require access to enough other parts of the game that checking other locations should naturally get the player to the soft-required level.
|
||||
- Buying expensive shop items might logically require access to a place where you can quickly farm money, or logically require access to enough parts of the game that checking other locations should naturally generate enough money without grinding.
|
||||
|
||||
Remember that all items referenced by logic (however hard or soft) must be `progression`. Since you typically don't want to turn a ton of `filler` items into `progression` just for this, it's common to e.g. write money logic using only the rare "$100" item, so the dozens of "$1" and "$10" items in your world can remain `filler`.
|
||||
|
||||
---
|
||||
|
||||
### What if my game has "missable" or "one-time-only" locations or region connections?
|
||||
|
||||
Archipelago logic assumes that once a region or location becomes reachable, it stays reachable forever, no matter what
|
||||
the player does in-game. Slightly more formally: Receiving an AP item must never cause a region connection or location
|
||||
to "go out of logic" (become unreachable when it was previously reachable), and receiving AP items is the only kind of
|
||||
state change that AP logic acknowledges. No other actions or events can change reachability.
|
||||
|
||||
So when the game itself does not follow this assumption, the options are:
|
||||
- Modify the game to make that location/connection repeatable
|
||||
- If there are both missable and repeatable ways to check the location/traverse the connection, then write logic for
|
||||
only the repeatable ways
|
||||
- Don't generate the missable location/connection at all
|
||||
- For connections, any logical regions will still need to be reachable through other, *repeatable* connections
|
||||
- For locations, this may require game changes to remove the vanilla item if it affects logic
|
||||
- Decide that resetting the save file is part of the game's logic, and warn players about that
|
||||
|
||||
@@ -11,13 +11,8 @@ found in the [general test directory](/test/general).
|
||||
## Defining World Tests
|
||||
|
||||
In order to run tests from your world, you will need to create a `test` package within your world package. This can be
|
||||
done by creating a `test` directory inside your world with an (empty) `__init__.py` inside it. By convention, a base
|
||||
for your world tests can be created in `bases.py` or any file that does not start with `test`, that you can then import
|
||||
into other modules. All tests should be defined in files named `test_*.py` (all lower case) and be member functions
|
||||
(named `test_*`) of classes (named `Test*` or `*Test`) that inherit from `unittest.TestCase` or a test base.
|
||||
|
||||
Defining anything inside `test/__init__.py` is deprecated. Defining TestBase there was previously the norm; however,
|
||||
it complicates test discovery because some worlds also put actual tests into `__init__.py`.
|
||||
done by creating a `test` directory with a file named `__init__.py` inside it inside your world. By convention, a base
|
||||
for your world tests can be created in this file that you can then import into other modules.
|
||||
|
||||
### WorldTestBase
|
||||
|
||||
@@ -26,7 +21,7 @@ interactions in the world interact as expected, you will want to use the [WorldT
|
||||
comes with the basics for test setup as well as a few preloaded tests that most worlds might want to check on varying
|
||||
options combinations.
|
||||
|
||||
Example `/worlds/<my_game>/test/bases.py`:
|
||||
Example `/worlds/<my_game>/test/__init__.py`:
|
||||
|
||||
```python
|
||||
from test.bases import WorldTestBase
|
||||
@@ -54,7 +49,7 @@ with `test_`.
|
||||
Example `/worlds/<my_game>/test/test_chest_access.py`:
|
||||
|
||||
```python
|
||||
from .bases import MyGameTestBase
|
||||
from . import MyGameTestBase
|
||||
|
||||
|
||||
class TestChestAccess(MyGameTestBase):
|
||||
@@ -124,12 +119,8 @@ variable to keep all the benefits of the test framework while not running the ma
|
||||
#### Using Pycharm
|
||||
|
||||
In PyCharm, running all tests can be done by right-clicking the root test directory and selecting Run 'Archipelago Unittests'.
|
||||
If you have never previously run ModuleUpdate.py, then you will need to do this once before the tests will run.
|
||||
You can run ModuleUpdate.py by right-clicking ModuleUpdate.py and selecting `Run 'ModuleUpdate'`.
|
||||
After running ModuleUpdate.py you may still get a `ModuleNotFoundError: No module named 'flask'` for the webhost tests.
|
||||
If this happens, run WebHost.py by right-clicking it and selecting `Run 'WebHost'`. Make sure to press enter when prompted.
|
||||
Unless you configured PyCharm to use pytest as a test runner, you may get import failures. To solve this,
|
||||
edit the run configuration, and set the working directory to the Archipelago directory which contains all the project files.
|
||||
Unless you configured PyCharm to use pytest as a test runner, you may get import failures. To solve this, edit the run configuration,
|
||||
and set the working directory to the Archipelago directory which contains all the project files.
|
||||
|
||||
If you only want to run your world's defined tests, repeat the steps for the test directory within your world.
|
||||
Your working directory should be the directory of your world in the worlds directory and the script should be the
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
[pytest]
|
||||
python_files = test_*.py Test*.py __init__.py # TODO: remove Test* once all worlds have been ported
|
||||
python_files = test_*.py Test*.py # TODO: remove Test* once all worlds have been ported
|
||||
python_classes = Test
|
||||
python_functions = test
|
||||
testpaths =
|
||||
|
||||
@@ -7,11 +7,10 @@ schema>=0.7.7
|
||||
kivy>=2.3.1
|
||||
bsdiff4>=1.2.6
|
||||
platformdirs>=4.3.6
|
||||
certifi>=2025.4.26
|
||||
certifi>=2025.1.31
|
||||
cython>=3.0.12
|
||||
cymem>=2.0.11
|
||||
orjson>=3.10.15
|
||||
discord-webhook>=1.3.0
|
||||
typing_extensions>=4.12.2
|
||||
pyshortcuts>=1.9.1
|
||||
kivymd @ git+https://github.com/kivymd/KivyMD@5ff9d0d
|
||||
|
||||
59
settings.py
59
settings.py
@@ -10,10 +10,9 @@ import sys
|
||||
import types
|
||||
import typing
|
||||
import warnings
|
||||
from collections.abc import Iterator, Sequence
|
||||
from enum import IntEnum
|
||||
from threading import Lock
|
||||
from typing import cast, Any, BinaryIO, ClassVar, TextIO, TypeVar, Union
|
||||
from typing import cast, Any, BinaryIO, ClassVar, Dict, Iterator, List, Optional, TextIO, Tuple, Union, TypeVar
|
||||
|
||||
__all__ = [
|
||||
"get_settings", "fmt_doc", "no_gui",
|
||||
@@ -24,7 +23,7 @@ __all__ = [
|
||||
|
||||
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: Dict[str, str] = {} # TODO: cache on disk and update when worlds change
|
||||
_world_settings_name_cache_updated = False
|
||||
_lock = Lock()
|
||||
|
||||
@@ -54,7 +53,7 @@ def fmt_doc(cls: type, level: int) -> str:
|
||||
|
||||
|
||||
class Group:
|
||||
_type_cache: ClassVar[dict[str, Any] | None] = None
|
||||
_type_cache: ClassVar[Optional[Dict[str, Any]]] = None
|
||||
_dumping: bool = False
|
||||
_has_attr: bool = False
|
||||
_changed: bool = False
|
||||
@@ -107,7 +106,7 @@ class Group:
|
||||
self.__dict__.values()))
|
||||
|
||||
@classmethod
|
||||
def get_type_hints(cls) -> dict[str, Any]:
|
||||
def get_type_hints(cls) -> Dict[str, Any]:
|
||||
"""Returns resolved type hints for the class"""
|
||||
if cls._type_cache is None:
|
||||
if not cls.__annotations__ or not isinstance(next(iter(cls.__annotations__.values())), str):
|
||||
@@ -125,10 +124,10 @@ class Group:
|
||||
return self[key]
|
||||
return default
|
||||
|
||||
def items(self) -> list[tuple[str, Any]]:
|
||||
def items(self) -> List[Tuple[str, Any]]:
|
||||
return [(key, getattr(self, key)) for key in self]
|
||||
|
||||
def update(self, dct: dict[str, Any]) -> None:
|
||||
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."
|
||||
|
||||
@@ -197,7 +196,7 @@ class Group:
|
||||
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]:
|
||||
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
|
||||
@@ -212,7 +211,7 @@ class Group:
|
||||
f.write(f"{indent}{yaml_line}")
|
||||
|
||||
@classmethod
|
||||
def _dump_item(cls, name: str | None, attr: object, f: TextIO, level: int) -> None:
|
||||
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
|
||||
@@ -224,7 +223,7 @@ class Group:
|
||||
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)
|
||||
pairs = cast(List[Tuple[ScalarNode, Any]], res.value)
|
||||
for k, v in pairs:
|
||||
k.style = None # remove quotes from keys
|
||||
return res
|
||||
@@ -330,9 +329,9 @@ class Path(str):
|
||||
"""Marks the file as required and opens a file browser when missing"""
|
||||
is_exe: bool = False
|
||||
"""Special cross-platform handling for executables"""
|
||||
description: str | None = None
|
||||
description: Optional[str] = None
|
||||
"""Title to display when browsing for the file"""
|
||||
copy_to: str | None = None
|
||||
copy_to: Optional[str] = None
|
||||
"""If not None, copy to AP folder instead of linking it"""
|
||||
|
||||
@classmethod
|
||||
@@ -340,7 +339,7 @@ class Path(str):
|
||||
"""Overload and raise to validate input files from browse"""
|
||||
pass
|
||||
|
||||
def browse(self: T, **kwargs: Any) -> T | None:
|
||||
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__}")
|
||||
|
||||
@@ -370,12 +369,12 @@ class _LocalPath(str):
|
||||
class FilePath(Path):
|
||||
# path to a file
|
||||
|
||||
md5s: ClassVar[list[str | bytes]] = []
|
||||
md5s: ClassVar[List[Union[str, bytes]]] = []
|
||||
"""MD5 hashes for default validator."""
|
||||
|
||||
def browse(self: T,
|
||||
filetypes: Sequence[tuple[str, Sequence[str]]] | None = None, **kwargs: Any)\
|
||||
-> T | None:
|
||||
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:
|
||||
@@ -440,7 +439,7 @@ class FilePath(Path):
|
||||
class FolderPath(Path):
|
||||
# path to a folder
|
||||
|
||||
def browse(self: T, **kwargs: Any) -> T | None:
|
||||
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:
|
||||
@@ -598,16 +597,16 @@ class ServerOptions(Group):
|
||||
OFF = 0
|
||||
ON = 1
|
||||
|
||||
host: str | None = None
|
||||
host: Optional[str] = None
|
||||
port: int = 38281
|
||||
password: str | None = None
|
||||
multidata: str | None = None
|
||||
savefile: str | None = None
|
||||
password: Optional[str] = None
|
||||
multidata: Optional[str] = None
|
||||
savefile: Optional[str] = None
|
||||
disable_save: bool = False
|
||||
loglevel: str = "info"
|
||||
logtime: bool = False
|
||||
server_password: ServerPassword | None = None
|
||||
disable_item_cheat: DisableItemCheat | bool = False
|
||||
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("auto")
|
||||
@@ -703,7 +702,7 @@ does nothing if not found
|
||||
"""
|
||||
|
||||
sni_path: SNIPath = SNIPath("SNI")
|
||||
snes_rom_start: SnesRomStart | bool = True
|
||||
snes_rom_start: Union[SnesRomStart, bool] = True
|
||||
|
||||
|
||||
class BizHawkClientOptions(Group):
|
||||
@@ -722,7 +721,7 @@ class BizHawkClientOptions(Group):
|
||||
"""
|
||||
|
||||
emuhawk_path: EmuHawkPath = EmuHawkPath(None)
|
||||
rom_start: RomStart | bool = True
|
||||
rom_start: Union[RomStart, bool] = True
|
||||
|
||||
|
||||
# Top-level group with lazy loading of worlds
|
||||
@@ -734,7 +733,7 @@ class Settings(Group):
|
||||
sni_options: SNIOptions = SNIOptions()
|
||||
bizhawkclient_options: BizHawkClientOptions = BizHawkClientOptions()
|
||||
|
||||
_filename: str | None = None
|
||||
_filename: Optional[str] = None
|
||||
|
||||
def __getattribute__(self, key: str) -> Any:
|
||||
if key.startswith("_") or key in self.__class__.__dict__:
|
||||
@@ -788,7 +787,7 @@ class Settings(Group):
|
||||
|
||||
return super().__getattribute__(key)
|
||||
|
||||
def __init__(self, location: str | None): # change to PathLike[str] once we drop 3.8?
|
||||
def __init__(self, location: Optional[str]): # change to PathLike[str] once we drop 3.8?
|
||||
super().__init__()
|
||||
if location:
|
||||
from Utils import parse_yaml
|
||||
@@ -822,7 +821,7 @@ class Settings(Group):
|
||||
import atexit
|
||||
atexit.register(autosave)
|
||||
|
||||
def save(self, location: str | None = None) -> None: # as above
|
||||
def save(self, location: Optional[str] = None) -> None: # as above
|
||||
from Utils import parse_yaml
|
||||
location = location or self._filename
|
||||
assert location, "No file specified"
|
||||
@@ -855,7 +854,7 @@ class Settings(Group):
|
||||
super().dump(f, level)
|
||||
|
||||
@property
|
||||
def filename(self) -> str | None:
|
||||
def filename(self) -> Optional[str]:
|
||||
return self._filename
|
||||
|
||||
|
||||
@@ -868,7 +867,7 @@ def get_settings() -> Settings:
|
||||
if not res:
|
||||
from Utils import user_path, local_path
|
||||
filenames = ("options.yaml", "host.yaml")
|
||||
locations: list[str] = []
|
||||
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]
|
||||
|
||||
46
setup.py
46
setup.py
@@ -1,20 +1,22 @@
|
||||
import base64
|
||||
import datetime
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
import platform
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import sysconfig
|
||||
import threading
|
||||
import urllib.request
|
||||
import warnings
|
||||
import zipfile
|
||||
from collections.abc import Iterable, Sequence
|
||||
import urllib.request
|
||||
import io
|
||||
import json
|
||||
import threading
|
||||
import subprocess
|
||||
|
||||
from hashlib import sha3_512
|
||||
from pathlib import Path
|
||||
from typing import Dict, Iterable, List, Optional, Sequence, Set, Tuple, Union
|
||||
|
||||
|
||||
# This is a bit jank. We need cx-Freeze to be able to run anything from this script, so install it
|
||||
requirement = 'cx-Freeze==8.0.0'
|
||||
@@ -58,7 +60,7 @@ from Cython.Build import cythonize
|
||||
|
||||
|
||||
# On Python < 3.10 LogicMixin is not currently supported.
|
||||
non_apworlds: set[str] = {
|
||||
non_apworlds: Set[str] = {
|
||||
"A Link to the Past",
|
||||
"Adventure",
|
||||
"ArchipIDLE",
|
||||
@@ -145,7 +147,7 @@ def download_SNI() -> None:
|
||||
print(f"No SNI found for system spec {platform_name} {machine_name}")
|
||||
|
||||
|
||||
signtool: str | None
|
||||
signtool: Optional[str]
|
||||
if os.path.exists("X:/pw.txt"):
|
||||
print("Using signtool")
|
||||
with open("X:/pw.txt", encoding="utf-8-sig") as f:
|
||||
@@ -203,7 +205,7 @@ def remove_sprites_from_folder(folder: Path) -> None:
|
||||
os.remove(folder / file)
|
||||
|
||||
|
||||
def _threaded_hash(filepath: str | Path) -> str:
|
||||
def _threaded_hash(filepath: Union[str, Path]) -> str:
|
||||
hasher = sha3_512()
|
||||
hasher.update(open(filepath, "rb").read())
|
||||
return base64.b85encode(hasher.digest()).decode()
|
||||
@@ -253,7 +255,7 @@ class BuildExeCommand(cx_Freeze.command.build_exe.build_exe):
|
||||
self.libfolder = Path(self.buildfolder, "lib")
|
||||
self.library = Path(self.libfolder, "library.zip")
|
||||
|
||||
def installfile(self, path: Path, subpath: str | Path | None = None, keep_content: bool = False) -> None:
|
||||
def installfile(self, path: Path, subpath: Optional[Union[str, Path]] = None, keep_content: bool = False) -> None:
|
||||
folder = self.buildfolder
|
||||
if subpath:
|
||||
folder /= subpath
|
||||
@@ -372,7 +374,7 @@ class BuildExeCommand(cx_Freeze.command.build_exe.build_exe):
|
||||
from worlds.AutoWorld import AutoWorldRegister
|
||||
assert not non_apworlds - set(AutoWorldRegister.world_types), \
|
||||
f"Unknown world {non_apworlds - set(AutoWorldRegister.world_types)} designated for .apworld"
|
||||
folders_to_remove: list[str] = []
|
||||
folders_to_remove: List[str] = []
|
||||
disabled_worlds_folder = "worlds_disabled"
|
||||
for entry in os.listdir(disabled_worlds_folder):
|
||||
if os.path.isdir(os.path.join(disabled_worlds_folder, entry)):
|
||||
@@ -444,12 +446,12 @@ class AppImageCommand(setuptools.Command):
|
||||
("app-exec=", None, "The application to run inside the image."),
|
||||
("yes", "y", 'Answer "yes" to all questions.'),
|
||||
]
|
||||
build_folder: Path | None
|
||||
dist_file: Path | None
|
||||
app_dir: Path | None
|
||||
build_folder: Optional[Path]
|
||||
dist_file: Optional[Path]
|
||||
app_dir: Optional[Path]
|
||||
app_name: str
|
||||
app_exec: Path | None
|
||||
app_icon: Path | None # source file
|
||||
app_exec: Optional[Path]
|
||||
app_icon: Optional[Path] # source file
|
||||
app_id: str # lower case name, used for icon and .desktop
|
||||
yes: bool
|
||||
|
||||
@@ -491,7 +493,7 @@ $APPDIR/$exe "$@"
|
||||
""")
|
||||
launcher_filename.chmod(0o755)
|
||||
|
||||
def install_icon(self, src: Path, name: str | None = None, symlink: Path | None = None) -> None:
|
||||
def install_icon(self, src: Path, name: Optional[str] = None, symlink: Optional[Path] = None) -> None:
|
||||
assert self.app_dir, "Invalid app_dir"
|
||||
try:
|
||||
from PIL import Image
|
||||
@@ -554,7 +556,7 @@ $APPDIR/$exe "$@"
|
||||
subprocess.call(f'ARCH={build_arch} ./appimagetool -n "{self.app_dir}" "{self.dist_file}"', shell=True)
|
||||
|
||||
|
||||
def find_libs(*args: str) -> Sequence[tuple[str, str]]:
|
||||
def find_libs(*args: str) -> Sequence[Tuple[str, str]]:
|
||||
"""Try to find system libraries to be included."""
|
||||
if not args:
|
||||
return []
|
||||
@@ -562,7 +564,7 @@ def find_libs(*args: str) -> Sequence[tuple[str, str]]:
|
||||
arch = build_arch.replace('_', '-')
|
||||
libc = 'libc6' # we currently don't support musl
|
||||
|
||||
def parse(line: str) -> tuple[tuple[str, str, str], str]:
|
||||
def parse(line: str) -> Tuple[Tuple[str, str, str], str]:
|
||||
lib, path = line.strip().split(' => ')
|
||||
lib, typ = lib.split(' ', 1)
|
||||
for test_arch in ('x86-64', 'i386', 'aarch64'):
|
||||
@@ -587,8 +589,8 @@ def find_libs(*args: str) -> Sequence[tuple[str, str]]:
|
||||
k: v for k, v in (parse(line) for line in data if "=>" in line)
|
||||
}
|
||||
|
||||
def find_lib(lib: str, arch: str, libc: str) -> str | None:
|
||||
cache: dict[tuple[str, str, str], str] = getattr(find_libs, "cache")
|
||||
def find_lib(lib: str, arch: str, libc: str) -> Optional[str]:
|
||||
cache: Dict[Tuple[str, str, str], str] = getattr(find_libs, "cache")
|
||||
for k, v in cache.items():
|
||||
if k == (lib, arch, libc):
|
||||
return v
|
||||
@@ -597,7 +599,7 @@ def find_libs(*args: str) -> Sequence[tuple[str, str]]:
|
||||
return v
|
||||
return None
|
||||
|
||||
res: list[tuple[str, str]] = []
|
||||
res: List[Tuple[str, str]] = []
|
||||
for arg in args:
|
||||
# try exact match, empty libc, empty arch, empty arch and libc
|
||||
file = find_lib(arg, arch, libc)
|
||||
|
||||
@@ -53,22 +53,6 @@ class TestImplemented(unittest.TestCase):
|
||||
if failed_world_loads:
|
||||
self.fail(f"The following worlds failed to load: {failed_world_loads}")
|
||||
|
||||
def test_prefill_items(self):
|
||||
"""Test that every world can reach every location from allstate before pre_fill."""
|
||||
for gamename, world_type in AutoWorldRegister.world_types.items():
|
||||
if gamename not in ("Archipelago", "Sudoku", "Final Fantasy", "Test Game"):
|
||||
with self.subTest(gamename):
|
||||
multiworld = setup_solo_multiworld(world_type, ("generate_early", "create_regions", "create_items",
|
||||
"set_rules", "connect_entrances", "generate_basic"))
|
||||
allstate = multiworld.get_all_state(False)
|
||||
locations = multiworld.get_locations()
|
||||
reachable = multiworld.get_reachable_locations(allstate)
|
||||
unreachable = [location for location in locations if location not in reachable]
|
||||
|
||||
self.assertTrue(not unreachable,
|
||||
f"Locations were not reachable with all state before prefill: "
|
||||
f"{unreachable}. Seed: {multiworld.seed}")
|
||||
|
||||
def test_explicit_indirect_conditions_spheres(self):
|
||||
"""Tests that worlds using explicit indirect conditions produce identical spheres as when using implicit
|
||||
indirect conditions"""
|
||||
|
||||
@@ -26,4 +26,4 @@ class TestBase(unittest.TestCase):
|
||||
for step in self.test_steps:
|
||||
with self.subTest("Step", step=step):
|
||||
call_all(multiworld, step)
|
||||
self.assertTrue(multiworld.get_all_state(False, allow_partial_entrances=True))
|
||||
self.assertTrue(multiworld.get_all_state(False, True))
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import re
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
from typing import Dict
|
||||
|
||||
|
||||
__all__ = ["copy", "delete"]
|
||||
|
||||
|
||||
_new_worlds: dict[str, str] = {}
|
||||
_new_worlds: Dict[str, str] = {}
|
||||
|
||||
|
||||
def copy(src: str, dst: str) -> None:
|
||||
|
||||
@@ -47,6 +47,17 @@ class TestCommonContext(unittest.IsolatedAsyncioTestCase):
|
||||
assert "Archipelago" in self.ctx.item_names, "Archipelago item names entry does not exist"
|
||||
assert "Archipelago" in self.ctx.location_names, "Archipelago location names entry does not exist"
|
||||
|
||||
async def test_implicit_name_lookups(self):
|
||||
# Items
|
||||
assert self.ctx.item_names[2**54 + 1] == "Test Item 1 - Safe"
|
||||
assert self.ctx.item_names[2**54 + 3] == f"Unknown item (ID: {2**54+3})"
|
||||
assert self.ctx.item_names[-1] == "Nothing"
|
||||
|
||||
# Locations
|
||||
assert self.ctx.location_names[2**54 + 1] == "Test Location 1 - Safe"
|
||||
assert self.ctx.location_names[2**54 + 3] == f"Unknown location (ID: {2**54+3})"
|
||||
assert self.ctx.location_names[-1] == "Cheat Console"
|
||||
|
||||
async def test_explicit_name_lookups(self):
|
||||
# Items
|
||||
assert self.ctx.item_names["__TestGame1"][2**54+1] == "Test Item 1 - Safe"
|
||||
|
||||
@@ -485,7 +485,7 @@ class World(metaclass=AutoWorldRegister):
|
||||
def get_filler_item_name(self) -> str:
|
||||
"""Called when the item pool needs to be filled with additional items to match location count."""
|
||||
logging.warning(f"World {self} is generating a filler item without custom filler pool.")
|
||||
return self.random.choice(tuple(self.item_name_to_id.keys()))
|
||||
return self.multiworld.random.choice(tuple(self.item_name_to_id.keys()))
|
||||
|
||||
@classmethod
|
||||
def create_group(cls, multiworld: "MultiWorld", new_player_id: int, players: Set[int]) -> World:
|
||||
|
||||
@@ -238,10 +238,10 @@ async def proxy_loop(ctx: AHITContext):
|
||||
logger.info("Aborting AHIT Proxy Client due to errors")
|
||||
|
||||
|
||||
def launch(*launch_args: str):
|
||||
def launch():
|
||||
async def main():
|
||||
parser = get_base_parser()
|
||||
args = parser.parse_args(launch_args)
|
||||
args = parser.parse_args()
|
||||
|
||||
ctx = AHITContext(args.connect, args.password)
|
||||
logger.info("Starting A Hat in Time proxy server")
|
||||
|
||||
@@ -16,9 +16,9 @@ from worlds.LauncherComponents import Component, components, icon_paths, launch
|
||||
from Utils import local_path
|
||||
|
||||
|
||||
def launch_client(*args: str):
|
||||
def launch_client():
|
||||
from .Client import launch
|
||||
launch_component(launch, name="AHITClient", args=args)
|
||||
launch_component(launch, name="AHITClient")
|
||||
|
||||
|
||||
components.append(Component("A Hat in Time Client", "AHITClient", func=launch_client,
|
||||
|
||||
@@ -54,13 +54,16 @@ def parse_arguments(argv, no_defaults=False):
|
||||
ret = parser.parse_args(argv)
|
||||
|
||||
# cannot be set through CLI currently
|
||||
ret.plando_items = []
|
||||
ret.plando_texts = {}
|
||||
ret.plando_connections = []
|
||||
|
||||
if multiargs.multi:
|
||||
defaults = copy.deepcopy(ret)
|
||||
for player in range(1, multiargs.multi + 1):
|
||||
playerargs = parse_arguments(shlex.split(getattr(ret, f"p{player}")), True)
|
||||
|
||||
for name in ["game", "sprite", "sprite_pool"]:
|
||||
for name in ["plando_items", "plando_texts", "plando_connections", "game", "sprite", "sprite_pool"]:
|
||||
value = getattr(defaults, name) if getattr(playerargs, name) is None else getattr(playerargs, name)
|
||||
if player == 1:
|
||||
setattr(ret, name, {1: value})
|
||||
|
||||
@@ -393,7 +393,9 @@ def global_rules(multiworld: MultiWorld, player: int):
|
||||
if world.options.pot_shuffle:
|
||||
# it could move the key to the top right platform which can only be reached with bombs
|
||||
add_rule(multiworld.get_location('Swamp Palace - Hookshot Pot Key', player), lambda state: can_use_bombs(state, player))
|
||||
set_rule(multiworld.get_entrance('Swamp Palace (West)', player), lambda state: state._lttp_has_key('Small Key (Swamp Palace)', player, 6))
|
||||
set_rule(multiworld.get_entrance('Swamp Palace (West)', player), lambda state: state._lttp_has_key('Small Key (Swamp Palace)', player, 6)
|
||||
if state.has('Hookshot', player)
|
||||
else state._lttp_has_key('Small Key (Swamp Palace)', player, 4))
|
||||
set_rule(multiworld.get_location('Swamp Palace - Big Chest', player), lambda state: state.has('Big Key (Swamp Palace)', player))
|
||||
if world.options.accessibility != 'full':
|
||||
allow_self_locking_items(multiworld.get_location('Swamp Palace - Big Chest', player), 'Big Key (Swamp Palace)')
|
||||
|
||||
@@ -505,20 +505,20 @@ class ALTTPWorld(World):
|
||||
def pre_fill(self):
|
||||
from Fill import fill_restrictive, FillError
|
||||
attempts = 5
|
||||
all_state = self.multiworld.get_all_state(use_cache=False)
|
||||
world = self.multiworld
|
||||
player = self.player
|
||||
all_state = world.get_all_state(use_cache=True)
|
||||
crystals = [self.create_item(name) for name in ['Red Pendant', 'Blue Pendant', 'Green Pendant', 'Crystal 1', 'Crystal 2', 'Crystal 3', 'Crystal 4', 'Crystal 7', 'Crystal 5', 'Crystal 6']]
|
||||
for crystal in crystals:
|
||||
all_state.remove(crystal)
|
||||
crystal_locations = [self.get_location('Turtle Rock - Prize'),
|
||||
self.get_location('Eastern Palace - Prize'),
|
||||
self.get_location('Desert Palace - Prize'),
|
||||
self.get_location('Tower of Hera - Prize'),
|
||||
self.get_location('Palace of Darkness - Prize'),
|
||||
self.get_location('Thieves\' Town - Prize'),
|
||||
self.get_location('Skull Woods - Prize'),
|
||||
self.get_location('Swamp Palace - Prize'),
|
||||
self.get_location('Ice Palace - Prize'),
|
||||
self.get_location('Misery Mire - Prize')]
|
||||
crystal_locations = [world.get_location('Turtle Rock - Prize', player),
|
||||
world.get_location('Eastern Palace - Prize', player),
|
||||
world.get_location('Desert Palace - Prize', player),
|
||||
world.get_location('Tower of Hera - Prize', player),
|
||||
world.get_location('Palace of Darkness - Prize', player),
|
||||
world.get_location('Thieves\' Town - Prize', player),
|
||||
world.get_location('Skull Woods - Prize', player),
|
||||
world.get_location('Swamp Palace - Prize', player),
|
||||
world.get_location('Ice Palace - Prize', player),
|
||||
world.get_location('Misery Mire - Prize', player)]
|
||||
placed_prizes = {loc.item.name for loc in crystal_locations if loc.item}
|
||||
unplaced_prizes = [crystal for crystal in crystals if crystal.name not in placed_prizes]
|
||||
empty_crystal_locations = [loc for loc in crystal_locations if not loc.item]
|
||||
@@ -526,8 +526,8 @@ class ALTTPWorld(World):
|
||||
try:
|
||||
prizepool = unplaced_prizes.copy()
|
||||
prize_locs = empty_crystal_locations.copy()
|
||||
self.multiworld.random.shuffle(prize_locs)
|
||||
fill_restrictive(self.multiworld, all_state, prize_locs, prizepool, True, lock=True,
|
||||
world.random.shuffle(prize_locs)
|
||||
fill_restrictive(world, all_state, prize_locs, prizepool, True, lock=True,
|
||||
name="LttP Dungeon Prizes")
|
||||
except FillError as e:
|
||||
lttp_logger.exception("Failed to place dungeon prizes (%s). Will retry %s more times", e,
|
||||
@@ -541,7 +541,7 @@ class ALTTPWorld(World):
|
||||
if self.options.mode == 'standard' and self.options.small_key_shuffle \
|
||||
and self.options.small_key_shuffle != small_key_shuffle.option_universal and \
|
||||
self.options.small_key_shuffle != small_key_shuffle.option_own_dungeons:
|
||||
self.multiworld.local_early_items[self.player]["Small Key (Hyrule Castle)"] = 1
|
||||
world.local_early_items[player]["Small Key (Hyrule Castle)"] = 1
|
||||
|
||||
@classmethod
|
||||
def stage_pre_fill(cls, world):
|
||||
@@ -811,15 +811,12 @@ class ALTTPWorld(World):
|
||||
return GetBeemizerItem(self.multiworld, self.player, item)
|
||||
|
||||
def get_pre_fill_items(self):
|
||||
res = [self.create_item(name) for name in ('Red Pendant', 'Blue Pendant', 'Green Pendant', 'Crystal 1',
|
||||
'Crystal 2', 'Crystal 3', 'Crystal 4', 'Crystal 7', 'Crystal 5',
|
||||
'Crystal 6')]
|
||||
res = []
|
||||
if self.dungeon_local_item_names:
|
||||
for dungeon in self.dungeons.values():
|
||||
for item in dungeon.all_items:
|
||||
if item.name in self.dungeon_local_item_names:
|
||||
res.append(item)
|
||||
|
||||
return res
|
||||
|
||||
def fill_slot_data(self):
|
||||
|
||||
@@ -24,7 +24,7 @@ class TestSwampPalace(TestDungeon):
|
||||
["Swamp Palace - Big Key Chest", False, [], ['Open Floodgate']],
|
||||
["Swamp Palace - Big Key Chest", False, [], ['Hammer']],
|
||||
["Swamp Palace - Big Key Chest", False, [], ['Small Key (Swamp Palace)']],
|
||||
["Swamp Palace - Big Key Chest", True, ['Open Floodgate', 'Small Key (Swamp Palace)', 'Small Key (Swamp Palace)', 'Small Key (Swamp Palace)', 'Small Key (Swamp Palace)', 'Small Key (Swamp Palace)', 'Small Key (Swamp Palace)', 'Flippers', 'Hammer']],
|
||||
["Swamp Palace - Big Key Chest", True, ['Open Floodgate', 'Small Key (Swamp Palace)', 'Small Key (Swamp Palace)', 'Small Key (Swamp Palace)', 'Small Key (Swamp Palace)', 'Flippers', 'Hammer']],
|
||||
|
||||
["Swamp Palace - Map Chest", False, []],
|
||||
["Swamp Palace - Map Chest", False, [], ['Flippers']],
|
||||
@@ -38,7 +38,7 @@ class TestSwampPalace(TestDungeon):
|
||||
["Swamp Palace - West Chest", False, [], ['Open Floodgate']],
|
||||
["Swamp Palace - West Chest", False, [], ['Hammer']],
|
||||
["Swamp Palace - West Chest", False, [], ['Small Key (Swamp Palace)']],
|
||||
["Swamp Palace - West Chest", True, ['Open Floodgate', 'Small Key (Swamp Palace)', 'Small Key (Swamp Palace)', 'Small Key (Swamp Palace)', 'Small Key (Swamp Palace)', 'Small Key (Swamp Palace)', 'Small Key (Swamp Palace)', 'Flippers', 'Hammer']],
|
||||
["Swamp Palace - West Chest", True, ['Open Floodgate', 'Small Key (Swamp Palace)', 'Small Key (Swamp Palace)', 'Small Key (Swamp Palace)', 'Small Key (Swamp Palace)', 'Flippers', 'Hammer']],
|
||||
|
||||
["Swamp Palace - Compass Chest", False, []],
|
||||
["Swamp Palace - Compass Chest", False, [], ['Flippers']],
|
||||
|
||||
@@ -207,6 +207,7 @@ class BlasphemousWorld(World):
|
||||
if not self.options.skill_randomizer:
|
||||
self.place_items_from_dict(skill_dict)
|
||||
|
||||
|
||||
def place_items_from_set(self, location_set: Set[str], name: str):
|
||||
for loc in location_set:
|
||||
self.get_location(loc).place_locked_item(self.create_item(name))
|
||||
|
||||
@@ -511,7 +511,7 @@ _vanilla_items = [
|
||||
DS3ItemData("Elkhorn Round Shield", 0x0133C510, DS3ItemCategory.SHIELD_INFUSIBLE),
|
||||
DS3ItemData("Warrior's Round Shield", 0x0133EC20, DS3ItemCategory.SHIELD_INFUSIBLE),
|
||||
DS3ItemData("Caduceus Round Shield", 0x01341330, DS3ItemCategory.SHIELD_INFUSIBLE),
|
||||
DS3ItemData("Red and White Round Shield", 0x01343A40, DS3ItemCategory.SHIELD_INFUSIBLE),
|
||||
DS3ItemData("Red and White Shield", 0x01343A40, DS3ItemCategory.SHIELD_INFUSIBLE),
|
||||
DS3ItemData("Blessed Red and White Shield+1", 0x01343FB9, DS3ItemCategory.SHIELD),
|
||||
DS3ItemData("Plank Shield", 0x01346150, DS3ItemCategory.SHIELD_INFUSIBLE),
|
||||
DS3ItemData("Leather Shield", 0x01348860, DS3ItemCategory.SHIELD_INFUSIBLE),
|
||||
|
||||
@@ -706,7 +706,7 @@ location_tables: Dict[str, List[DS3LocationData]] = {
|
||||
DS3LocationData("US: Whip - back alley, behind wooden wall", "Whip", hidden=True),
|
||||
DS3LocationData("US: Great Scythe - building by white tree, balcony", "Great Scythe"),
|
||||
DS3LocationData("US: Homeward Bone - foot, drop overlook", "Homeward Bone",
|
||||
static='02,0:53100950::'),
|
||||
static='02,0:53100540::'),
|
||||
DS3LocationData("US: Large Soul of a Deserted Corpse - around corner by Cliff Underside",
|
||||
"Large Soul of a Deserted Corpse", hidden=True), # Hidden corner
|
||||
DS3LocationData("US: Ember - behind burning tree", "Ember"),
|
||||
@@ -732,9 +732,8 @@ location_tables: Dict[str, List[DS3LocationData]] = {
|
||||
missable=True), # requires projectile
|
||||
DS3LocationData("US: Flame Stoneplate Ring - hanging corpse by Mound-Maker transport",
|
||||
"Flame Stoneplate Ring"),
|
||||
DS3LocationData("US: Red and White Round Shield - chasm, hanging corpse",
|
||||
"Red and White Round Shield", static="02,0:53100740::",
|
||||
missable=True), # requires projectile
|
||||
DS3LocationData("US: Red and White Shield - chasm, hanging corpse", "Red and White Shield",
|
||||
static="02,0:53100740::", missable=True), # requires projectile
|
||||
DS3LocationData("US: Small Leather Shield - first building, hanging corpse by entrance",
|
||||
"Small Leather Shield"),
|
||||
DS3LocationData("US: Pale Tongue - tower village, hanging corpse", "Pale Tongue"),
|
||||
|
||||
@@ -2239,7 +2239,7 @@ static _Dark Souls III_ randomizer].
|
||||
<tr><td>US: Pyromancy Flame - Cornyx</td><td>Given by Cornyx in Firelink Shrine or dropped.</td></tr>
|
||||
<tr><td>US: Red Bug Pellet - tower village building, basement</td><td>On the floor of the building after the Fire Demon encounter</td></tr>
|
||||
<tr><td>US: Red Hilted Halberd - chasm crypt</td><td>In the skeleton area accessible from Grave Key or dropping down from near Eygon</td></tr>
|
||||
<tr><td>US: Red and White Round Shield - chasm, hanging corpse</td><td>On a hanging corpse in the ravine accessible with the Grave Key or dropping down near Eygon, to the entrance of Irina's prison. Must be shot down with an arrow or projective.</td></tr>
|
||||
<tr><td>US: Red and White Shield - chasm, hanging corpse</td><td>On a hanging corpse in the ravine accessible with the Grave Key or dropping down near Eygon, to the entrance of Irina's prison. Must be shot down with an arrow or projective.</td></tr>
|
||||
<tr><td>US: Reinforced Club - by white tree</td><td>Near the Birch Tree where giant shoots arrows</td></tr>
|
||||
<tr><td>US: Repair Powder - first building, balcony</td><td>On the balcony of the first Undead Settlement building</td></tr>
|
||||
<tr><td>US: Rusted Coin - awning above Dilapidated Bridge</td><td>On a wooden ledge near the Dilapidated Bridge bonfire. Must be jumped to from near Cathedral Evangelist enemy</td></tr>
|
||||
|
||||
@@ -9,6 +9,7 @@ import random
|
||||
import re
|
||||
import string
|
||||
import subprocess
|
||||
|
||||
import sys
|
||||
import time
|
||||
import typing
|
||||
@@ -16,16 +17,15 @@ from queue import Queue
|
||||
|
||||
import factorio_rcon
|
||||
|
||||
import Utils
|
||||
from CommonClient import ClientCommandProcessor, CommonContext, logger, server_loop, gui_enabled, get_base_parser
|
||||
from MultiServer import mark_raw
|
||||
from NetUtils import ClientStatus, NetworkItem, JSONtoTextParser, JSONMessagePart
|
||||
from Utils import async_start, get_file_safe_name, is_windows, Version, format_SI_prefix, get_text_between
|
||||
from .settings import FactorioSettings
|
||||
from settings import get_settings
|
||||
from Utils import async_start, get_file_safe_name
|
||||
|
||||
|
||||
def check_stdin() -> None:
|
||||
if is_windows and sys.stdin:
|
||||
if Utils.is_windows and sys.stdin:
|
||||
print("WARNING: Console input is not routed reliably on Windows, use the GUI instead.")
|
||||
|
||||
|
||||
@@ -67,7 +67,7 @@ class FactorioContext(CommonContext):
|
||||
items_handling = 0b111 # full remote
|
||||
|
||||
# updated by spinup server
|
||||
mod_version: Version = Version(0, 0, 0)
|
||||
mod_version: Utils.Version = Utils.Version(0, 0, 0)
|
||||
|
||||
def __init__(self, server_address, password, filter_item_sends: bool, bridge_chat_out: bool):
|
||||
super(FactorioContext, self).__init__(server_address, password)
|
||||
@@ -133,7 +133,7 @@ class FactorioContext(CommonContext):
|
||||
elif self.current_energy_link_value is None:
|
||||
return "Standby"
|
||||
else:
|
||||
return f"{format_SI_prefix(self.current_energy_link_value)}J"
|
||||
return f"{Utils.format_SI_prefix(self.current_energy_link_value)}J"
|
||||
|
||||
def on_deathlink(self, data: dict):
|
||||
if self.rcon_client:
|
||||
@@ -155,10 +155,10 @@ class FactorioContext(CommonContext):
|
||||
if self.energy_link_increment and args.get("last_deplete", -1) == self.last_deplete:
|
||||
# it's our deplete request
|
||||
gained = int(args["original_value"] - args["value"])
|
||||
gained_text = format_SI_prefix(gained) + "J"
|
||||
gained_text = Utils.format_SI_prefix(gained) + "J"
|
||||
if gained:
|
||||
logger.debug(f"EnergyLink: Received {gained_text}. "
|
||||
f"{format_SI_prefix(args['value'])}J remaining.")
|
||||
f"{Utils.format_SI_prefix(args['value'])}J remaining.")
|
||||
self.rcon_client.send_command(f"/ap-energylink {gained}")
|
||||
|
||||
def on_user_say(self, text: str) -> typing.Optional[str]:
|
||||
@@ -278,7 +278,7 @@ async def game_watcher(ctx: FactorioContext):
|
||||
}]))
|
||||
ctx.rcon_client.send_command(
|
||||
f"/ap-energylink -{value}")
|
||||
logger.debug(f"EnergyLink: Sent {format_SI_prefix(value)}J")
|
||||
logger.debug(f"EnergyLink: Sent {Utils.format_SI_prefix(value)}J")
|
||||
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
@@ -439,9 +439,9 @@ async def factorio_spinup_server(ctx: FactorioContext) -> bool:
|
||||
factorio_server_logger.info(msg)
|
||||
if "Loading mod AP-" in msg and msg.endswith("(data.lua)"):
|
||||
parts = msg.split()
|
||||
ctx.mod_version = Version(*(int(number) for number in parts[-2].split(".")))
|
||||
ctx.mod_version = Utils.Version(*(int(number) for number in parts[-2].split(".")))
|
||||
elif "Write data path: " in msg:
|
||||
ctx.write_data_path = get_text_between(msg, "Write data path: ", " [")
|
||||
ctx.write_data_path = Utils.get_text_between(msg, "Write data path: ", " [")
|
||||
if "AppData" in ctx.write_data_path:
|
||||
logger.warning("It appears your mods are loaded from Appdata, "
|
||||
"this can lead to problems with multiple Factorio instances. "
|
||||
@@ -521,16 +521,10 @@ rcon_port = args.rcon_port
|
||||
rcon_password = args.rcon_password if args.rcon_password else ''.join(
|
||||
random.choice(string.ascii_letters) for x in range(32))
|
||||
factorio_server_logger = logging.getLogger("FactorioServer")
|
||||
settings: FactorioSettings = get_settings().factorio_options
|
||||
if os.path.samefile(settings.executable, sys.executable):
|
||||
selected_executable = settings.executable
|
||||
settings.executable = FactorioSettings.executable # reset to default
|
||||
raise Exception(f"FactorioClient was set to run itself {selected_executable}, aborting process bomb.")
|
||||
|
||||
executable = settings.executable
|
||||
|
||||
options = Utils.get_settings()
|
||||
executable = options["factorio_options"]["executable"]
|
||||
server_settings = args.server_settings if args.server_settings \
|
||||
else getattr(settings, "server_settings", None)
|
||||
else options["factorio_options"].get("server_settings", None)
|
||||
server_args = ("--rcon-port", rcon_port, "--rcon-password", rcon_password)
|
||||
|
||||
|
||||
@@ -541,8 +535,12 @@ def launch():
|
||||
|
||||
if server_settings:
|
||||
server_settings = os.path.abspath(server_settings)
|
||||
initial_filter_item_sends = bool(settings.filter_item_sends)
|
||||
initial_bridge_chat_out = bool(settings.bridge_chat_out)
|
||||
if not isinstance(options["factorio_options"]["filter_item_sends"], bool):
|
||||
logging.warning(f"Warning: Option filter_item_sends should be a bool.")
|
||||
initial_filter_item_sends = bool(options["factorio_options"]["filter_item_sends"])
|
||||
if not isinstance(options["factorio_options"]["bridge_chat_out"], bool):
|
||||
logging.warning(f"Warning: Option bridge_chat_out should be a bool.")
|
||||
initial_bridge_chat_out = bool(options["factorio_options"]["bridge_chat_out"])
|
||||
|
||||
if not os.path.exists(os.path.dirname(executable)):
|
||||
raise FileNotFoundError(f"Path {os.path.dirname(executable)} does not exist or could not be accessed.")
|
||||
|
||||
@@ -5,6 +5,7 @@ import logging
|
||||
import typing
|
||||
|
||||
import Utils
|
||||
import settings
|
||||
from BaseClasses import Region, Location, Item, Tutorial, ItemClassification
|
||||
from worlds.AutoWorld import World, WebWorld
|
||||
from worlds.LauncherComponents import Component, components, Type, launch as launch_component
|
||||
@@ -19,7 +20,6 @@ from .Technologies import base_tech_table, recipe_sources, base_technology_table
|
||||
progressive_technology_table, common_tech_table, tech_to_progressive_lookup, progressive_tech_table, \
|
||||
get_science_pack_pools, Recipe, recipes, technology_table, tech_table, factorio_base_id, useless_technologies, \
|
||||
fluids, stacking_items, valid_ingredients, progressive_rows
|
||||
from .settings import FactorioSettings
|
||||
|
||||
|
||||
def launch_client():
|
||||
@@ -27,7 +27,30 @@ def launch_client():
|
||||
launch_component(launch, name="FactorioClient")
|
||||
|
||||
|
||||
components.append(Component("Factorio Client", func=launch_client, component_type=Type.CLIENT))
|
||||
components.append(Component("Factorio Client", "FactorioClient", func=launch_client, component_type=Type.CLIENT))
|
||||
|
||||
|
||||
class FactorioSettings(settings.Group):
|
||||
class Executable(settings.UserFilePath):
|
||||
is_exe = True
|
||||
|
||||
class ServerSettings(settings.OptionalUserFilePath):
|
||||
"""
|
||||
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"
|
||||
"""
|
||||
|
||||
class FilterItemSends(settings.Bool):
|
||||
"""Whether to filter item send messages displayed in-game to only those that involve you."""
|
||||
|
||||
class BridgeChatOut(settings.Bool):
|
||||
"""Whether to send chat messages from players on the Factorio server to Archipelago."""
|
||||
|
||||
executable: Executable = Executable("factorio/bin/x64/factorio")
|
||||
server_settings: typing.Optional[FactorioSettings.ServerSettings] = None
|
||||
filter_item_sends: typing.Union[FilterItemSends, bool] = False
|
||||
bridge_chat_out: typing.Union[BridgeChatOut, bool] = True
|
||||
|
||||
|
||||
class FactorioWeb(WebWorld):
|
||||
@@ -92,7 +115,6 @@ class Factorio(World):
|
||||
settings: typing.ClassVar[FactorioSettings]
|
||||
trap_names: tuple[str] = ("Evolution", "Attack", "Teleport", "Grenade", "Cluster Grenade", "Artillery",
|
||||
"Atomic Rocket", "Atomic Cliff Remover", "Inventory Spill")
|
||||
want_progressives: dict[str, bool] = collections.defaultdict(lambda: False)
|
||||
|
||||
def __init__(self, world, player: int):
|
||||
super(Factorio, self).__init__(world, player)
|
||||
@@ -111,8 +133,6 @@ class Factorio(World):
|
||||
self.options.max_tech_cost.value, self.options.min_tech_cost.value
|
||||
self.tech_mix = self.options.tech_cost_mix.value
|
||||
self.skip_silo = self.options.silo.value == Silo.option_spawn
|
||||
self.want_progressives = collections.defaultdict(
|
||||
lambda: self.options.progressive.want_progressives(self.random))
|
||||
|
||||
def create_regions(self):
|
||||
player = self.player
|
||||
@@ -181,6 +201,9 @@ class Factorio(World):
|
||||
range(getattr(self.options,
|
||||
f"{trap_name.lower().replace(' ', '_')}_traps")))
|
||||
|
||||
want_progressives = collections.defaultdict(lambda: self.options.progressive.
|
||||
want_progressives(self.random))
|
||||
|
||||
cost_sorted_locations = sorted(self.science_locations, key=lambda location: location.name)
|
||||
special_index = {"automation": 0,
|
||||
"logistics": 1,
|
||||
@@ -195,7 +218,7 @@ class Factorio(World):
|
||||
for tech_name in base_tech_table:
|
||||
if tech_name not in self.removed_technologies:
|
||||
progressive_item_name = tech_to_progressive_lookup.get(tech_name, tech_name)
|
||||
want_progressive = self.want_progressives[progressive_item_name]
|
||||
want_progressive = want_progressives[progressive_item_name]
|
||||
item_name = progressive_item_name if want_progressive else tech_name
|
||||
tech_item = self.create_item(item_name)
|
||||
index = special_index.get(tech_name, None)
|
||||
@@ -210,12 +233,6 @@ class Factorio(World):
|
||||
loc.place_locked_item(tech_item)
|
||||
loc.revealed = True
|
||||
|
||||
def get_filler_item_name(self) -> str:
|
||||
tech_name: str = self.random.choice(tuple(tech_table))
|
||||
progressive_item_name: str = tech_to_progressive_lookup.get(tech_name, tech_name)
|
||||
want_progressive: bool = self.want_progressives[progressive_item_name]
|
||||
return progressive_item_name if want_progressive else tech_name
|
||||
|
||||
def set_rules(self):
|
||||
player = self.player
|
||||
shapes = get_shapes(self)
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
import typing
|
||||
|
||||
import settings
|
||||
|
||||
|
||||
class FactorioSettings(settings.Group):
|
||||
class Executable(settings.UserFilePath):
|
||||
is_exe = True
|
||||
|
||||
class ServerSettings(settings.OptionalUserFilePath):
|
||||
"""
|
||||
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"
|
||||
"""
|
||||
|
||||
class FilterItemSends(settings.Bool):
|
||||
"""Whether to filter item send messages displayed in-game to only those that involve you."""
|
||||
|
||||
class BridgeChatOut(settings.Bool):
|
||||
"""Whether to send chat messages from players on the Factorio server to Archipelago."""
|
||||
|
||||
executable: Executable = Executable("factorio/bin/x64/factorio")
|
||||
server_settings: typing.Optional[ServerSettings] = None
|
||||
filter_item_sends: typing.Union[FilterItemSends, bool] = False
|
||||
bridge_chat_out: typing.Union[BridgeChatOut, bool] = True
|
||||
@@ -20,11 +20,9 @@ It is generally recommended that you use a virtual environment to run python bas
|
||||
3. Run the command `source venv/bin/activate` to activate the virtual environment.
|
||||
4. If you want to exit the virtual environment, run the command `deactivate`.
|
||||
## Steps to Run the Clients
|
||||
1. Run the command `python3 Launcher.py`.
|
||||
2. If your game doesn't have a patch file, just click the desired client in the right side column.
|
||||
3. If your game does have a patch file, click the 'Open Patch' button and navigate to your patch file (the filename extension will look something like apsm, aplttp, apsmz3, etc.).
|
||||
4. If the patching process needs a rom, but cannot find it, it will ask you to navigate to your legally obtained rom.
|
||||
5. Your client should now be running and rom created (where applicable).
|
||||
1. If your game doesn't have a patch file, run the command `python3 SNIClient.py`, changing the filename with the file of the client you want to run.
|
||||
2. If your game does have a patch file, move the base rom to the Archipelago directory and run the command `python3 SNIClient.py 'patchfile'` with the filename extension for the patch file (apsm, aplttp, apsmz3, etc.) included and changing the filename with the file of the client you want to run.
|
||||
3. Your client should now be running and rom created (where applicable).
|
||||
## Additional Steps for SNES Games
|
||||
1. If using RetroArch, the instructions to set up your emulator [here in the Link to the Past setup guide](https://archipelago.gg/tutorial/A%20Link%20to%20the%20Past/multiworld/en) also work on the macOS version of RetroArch.
|
||||
2. Double click on the SNI tar.gz download to extract the files to an SNI directory. If it isn't already, rename this directory to SNI to make some steps easier.
|
||||
|
||||
@@ -127,10 +127,6 @@ class Hylics2World(World):
|
||||
tv = tvs.pop()
|
||||
self.get_location(tv).place_locked_item(self.create_item(gesture))
|
||||
|
||||
def get_pre_fill_items(self) -> List["Item"]:
|
||||
if self.options.gesture_shuffle:
|
||||
return [self.create_item(gesture["name"]) for gesture in Items.gesture_item_table.values()]
|
||||
return []
|
||||
|
||||
def fill_slot_data(self) -> Dict[str, Any]:
|
||||
slot_data: Dict[str, Any] = {
|
||||
|
||||
@@ -436,10 +436,6 @@ class KH2World(World):
|
||||
for location in keyblade_locations:
|
||||
location.locked = True
|
||||
|
||||
def get_pre_fill_items(self) -> List["Item"]:
|
||||
return [self.create_item(item) for item in [*DonaldAbility_Table.keys(), *GoofyAbility_Table.keys(),
|
||||
*SupportAbility_Table.keys()]]
|
||||
|
||||
def starting_invo_verify(self):
|
||||
"""
|
||||
Making sure the player doesn't put too many abilities in their starting inventory.
|
||||
|
||||
@@ -151,7 +151,8 @@ class ItemTracker:
|
||||
def __init__(self, gameboy) -> None:
|
||||
self.gameboy = gameboy
|
||||
self.loadItems()
|
||||
self.extraItems = {}
|
||||
pass
|
||||
extraItems = {}
|
||||
|
||||
async def readRamByte(self, byte):
|
||||
return (await self.gameboy.read_memory_cache([byte]))[byte]
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from typing import Optional
|
||||
|
||||
from Fill import parse_planned_blocks, distribute_planned_blocks, resolve_early_locations_for_planned
|
||||
from Options import PlandoItems
|
||||
from Fill import distribute_planned
|
||||
from test.general import setup_solo_multiworld
|
||||
from worlds.AutoWorld import call_all
|
||||
from . import LADXTestBase
|
||||
@@ -20,17 +19,14 @@ class PlandoTest(LADXTestBase):
|
||||
],
|
||||
}],
|
||||
}
|
||||
|
||||
|
||||
def world_setup(self, seed: Optional[int] = None) -> None:
|
||||
self.multiworld = setup_solo_multiworld(
|
||||
LinksAwakeningWorld,
|
||||
("generate_early", "create_regions", "create_items", "set_rules", "generate_basic")
|
||||
)
|
||||
self.multiworld.worlds[1].options.plando_items = PlandoItems.from_any(self.options["plando_items"])
|
||||
self.multiworld.plando_item_blocks = parse_planned_blocks(self.multiworld)
|
||||
resolve_early_locations_for_planned(self.multiworld)
|
||||
distribute_planned_blocks(self.multiworld, [x for player in self.multiworld.plando_item_blocks
|
||||
for x in self.multiworld.plando_item_blocks[player]])
|
||||
self.multiworld.plando_items[1] = self.options["plando_items"]
|
||||
distribute_planned(self.multiworld)
|
||||
call_all(self.multiworld, "pre_fill")
|
||||
|
||||
def test_planned(self):
|
||||
|
||||
@@ -16,8 +16,8 @@ from .portals import PORTALS, add_closed_portal_reqs, disconnect_portals, shuffl
|
||||
from .regions import LEVELS, MEGA_SHARDS, LOCATIONS, REGION_CONNECTIONS
|
||||
from .rules import MessengerHardRules, MessengerOOBRules, MessengerRules
|
||||
from .shop import FIGURINES, PROG_SHOP_ITEMS, SHOP_ITEMS, USEFUL_SHOP_ITEMS, shuffle_shop_prices
|
||||
from .subclasses import MessengerItem, MessengerRegion, MessengerShopLocation
|
||||
from .transitions import disconnect_entrances, shuffle_transitions
|
||||
from .subclasses import MessengerEntrance, MessengerItem, MessengerRegion, MessengerShopLocation
|
||||
from .transitions import shuffle_transitions
|
||||
|
||||
components.append(
|
||||
Component("The Messenger", component_type=Type.CLIENT, func=launch_game, game_name="The Messenger", supports_uri=True)
|
||||
@@ -266,8 +266,6 @@ class MessengerWorld(World):
|
||||
# MessengerOOBRules(self).set_messenger_rules()
|
||||
|
||||
def connect_entrances(self) -> None:
|
||||
if self.options.shuffle_transitions:
|
||||
disconnect_entrances(self)
|
||||
add_closed_portal_reqs(self)
|
||||
# i need portal shuffle to happen after rules exist so i can validate it
|
||||
attempts = 5
|
||||
|
||||
@@ -23,12 +23,21 @@ These steps can also be followed to launch the game and check for mod updates af
|
||||
### Manual Installation
|
||||
|
||||
1. Download and install Courier Mod Loader using the instructions on the release page
|
||||
* [Latest release is currently 0.7.1](https://github.com/Brokemia/Courier/releases)
|
||||
* [Latest release is currently 0.7.1](https://github.com/Brokemia/Courier/releases)
|
||||
2. Download and install the randomizer mod
|
||||
1. Download the latest TheMessengerRandomizerAP.zip from
|
||||
[The Messenger Randomizer Mod AP releases page](https://github.com/alwaysintreble/TheMessengerRandomizerModAP/releases)
|
||||
2. Extract the zip file to `TheMessenger/Mods/` of your game's install location
|
||||
* You cannot have both the non-AP randomizer and the AP randomizer installed at the same time
|
||||
1. Download the latest TheMessengerRandomizerAP.zip from
|
||||
[The Messenger Randomizer Mod AP releases page](https://github.com/alwaysintreble/TheMessengerRandomizerModAP/releases)
|
||||
2. Extract the zip file to `TheMessenger/Mods/` of your game's install location
|
||||
* You cannot have both the non-AP randomizer and the AP randomizer installed at the same time
|
||||
3. Optionally, Backup your save game
|
||||
* On Windows
|
||||
1. Press `Windows Key + R` to open run
|
||||
2. Type `%appdata%` to access AppData
|
||||
3. Navigate to `AppData/locallow/SabotageStudios/The Messenger`
|
||||
4. Rename `SaveGame.txt` to any name of your choice
|
||||
* On Linux
|
||||
1. Navigate to `steamapps/compatdata/764790/pfx/drive_c/users/steamuser/AppData/LocalLow/Sabotage Studio/The Messenger`
|
||||
2. Rename `SaveGame.txt` to any name of your choice
|
||||
|
||||
## Joining a MultiWorld Game
|
||||
|
||||
@@ -48,15 +57,15 @@ These steps can also be followed to launch the game and check for mod updates af
|
||||
1. Launch the game
|
||||
2. Navigate to `Options > Archipelago Options`
|
||||
3. Enter connection info using the relevant option buttons
|
||||
* **The game is limited to alphanumerical characters, `.`, and `-`.**
|
||||
* This defaults to `archipelago.gg` and does not need to be manually changed if connecting to a game hosted on the
|
||||
website.
|
||||
* If using a name that cannot be entered in the in game menus, there is a config file (APConfig.toml) in the game
|
||||
directory. When using this, all connection information must be entered in the file.
|
||||
* **The game is limited to alphanumerical characters, `.`, and `-`.**
|
||||
* This defaults to `archipelago.gg` and does not need to be manually changed if connecting to a game hosted on the
|
||||
website.
|
||||
* If using a name that cannot be entered in the in game menus, there is a config file (APConfig.toml) in the game
|
||||
directory. When using this, all connection information must be entered in the file.
|
||||
4. Select the `Connect to Archipelago` button
|
||||
5. Navigate to save file selection
|
||||
6. Start a new game
|
||||
* If you're already connected, deleting an existing save will not disconnect you and is completely safe.
|
||||
* If you're already connected, deleting an existing save will not disconnect you and is completely safe.
|
||||
|
||||
## Continuing a MultiWorld Game
|
||||
|
||||
|
||||
@@ -292,10 +292,12 @@ def disconnect_portals(world: "MessengerWorld") -> None:
|
||||
|
||||
|
||||
def validate_portals(world: "MessengerWorld") -> bool:
|
||||
new_state = CollectionState(world.multiworld, True)
|
||||
if world.options.shuffle_transitions:
|
||||
return True
|
||||
new_state = CollectionState(world.multiworld)
|
||||
new_state.update_reachable_regions(world.player)
|
||||
reachable_locs = 0
|
||||
for loc in world.get_locations():
|
||||
for loc in world.multiworld.get_locations(world.player):
|
||||
reachable_locs += loc.can_reach(new_state)
|
||||
if reachable_locs > 5:
|
||||
return True
|
||||
|
||||
@@ -10,8 +10,25 @@ if TYPE_CHECKING:
|
||||
from . import MessengerWorld
|
||||
|
||||
|
||||
class MessengerEntrance(Entrance):
|
||||
world: "MessengerWorld | None" = None
|
||||
|
||||
def can_connect_to(self, other: Entrance, dead_end: bool, state: "ERPlacementState") -> bool:
|
||||
can_connect = super().can_connect_to(other, dead_end, state)
|
||||
world: MessengerWorld = getattr(self, "world", None)
|
||||
if not world or world.reachable_locs or not can_connect:
|
||||
return can_connect
|
||||
empty_state = CollectionState(world.multiworld, True)
|
||||
self.connected_region = other.connected_region
|
||||
empty_state.update_reachable_regions(world.player)
|
||||
world.reachable_locs = any(loc.can_reach(empty_state) and not loc.is_event for loc in world.get_locations())
|
||||
self.connected_region = None
|
||||
return world.reachable_locs and (not state.coupled or self.name != other.name)
|
||||
|
||||
|
||||
class MessengerRegion(Region):
|
||||
parent: str | None
|
||||
entrance_type = MessengerEntrance
|
||||
|
||||
def __init__(self, name: str, world: "MessengerWorld", parent: str | None = None) -> None:
|
||||
super().__init__(name, world.player, world.multiworld)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from BaseClasses import Entrance, Region
|
||||
from BaseClasses import Region
|
||||
from entrance_rando import EntranceType, randomize_entrances
|
||||
from .connections import RANDOMIZED_CONNECTIONS, TRANSITIONS
|
||||
from .options import ShuffleTransitions, TransitionPlando
|
||||
@@ -9,33 +9,6 @@ if TYPE_CHECKING:
|
||||
from . import MessengerWorld
|
||||
|
||||
|
||||
def disconnect_entrances(world: "MessengerWorld") -> None:
|
||||
def disconnect_entrance() -> None:
|
||||
child = entrance.connected_region.name
|
||||
child_region = entrance.connected_region
|
||||
child_region.entrances.remove(entrance)
|
||||
entrance.connected_region = None
|
||||
|
||||
er_type = EntranceType.ONE_WAY if child == "Glacial Peak - Left" else \
|
||||
EntranceType.TWO_WAY if child in RANDOMIZED_CONNECTIONS else EntranceType.ONE_WAY
|
||||
if er_type == EntranceType.TWO_WAY:
|
||||
mock_entrance = entrance.parent_region.create_er_target(entrance.name)
|
||||
else:
|
||||
mock_entrance = child_region.create_er_target(child)
|
||||
|
||||
entrance.randomization_type = er_type
|
||||
mock_entrance.randomization_type = er_type
|
||||
|
||||
|
||||
for parent, child in RANDOMIZED_CONNECTIONS.items():
|
||||
if child == "Corrupted Future":
|
||||
entrance = world.get_entrance("Artificer's Portal")
|
||||
elif child == "Tower of Time - Left":
|
||||
entrance = world.get_entrance("Artificer's Challenge")
|
||||
else:
|
||||
entrance = world.get_entrance(f"{parent} -> {child}")
|
||||
disconnect_entrance()
|
||||
|
||||
def connect_plando(world: "MessengerWorld", plando_connections: TransitionPlando) -> None:
|
||||
def remove_dangling_exit(region: Region) -> None:
|
||||
# find the disconnected exit and remove references to it
|
||||
@@ -86,6 +59,32 @@ def connect_plando(world: "MessengerWorld", plando_connections: TransitionPlando
|
||||
def shuffle_transitions(world: "MessengerWorld") -> None:
|
||||
coupled = world.options.shuffle_transitions == ShuffleTransitions.option_coupled
|
||||
|
||||
def disconnect_entrance() -> None:
|
||||
child_region.entrances.remove(entrance)
|
||||
entrance.connected_region = None
|
||||
|
||||
er_type = EntranceType.ONE_WAY if child == "Glacial Peak - Left" else \
|
||||
EntranceType.TWO_WAY if child in RANDOMIZED_CONNECTIONS else EntranceType.ONE_WAY
|
||||
if er_type == EntranceType.TWO_WAY:
|
||||
mock_entrance = parent_region.create_er_target(entrance.name)
|
||||
else:
|
||||
mock_entrance = child_region.create_er_target(child)
|
||||
|
||||
entrance.randomization_type = er_type
|
||||
mock_entrance.randomization_type = er_type
|
||||
|
||||
for parent, child in RANDOMIZED_CONNECTIONS.items():
|
||||
if child == "Corrupted Future":
|
||||
entrance = world.get_entrance("Artificer's Portal")
|
||||
elif child == "Tower of Time - Left":
|
||||
entrance = world.get_entrance("Artificer's Challenge")
|
||||
else:
|
||||
entrance = world.get_entrance(f"{parent} -> {child}")
|
||||
parent_region = entrance.parent_region
|
||||
child_region = entrance.connected_region
|
||||
entrance.world = world
|
||||
disconnect_entrance()
|
||||
|
||||
plando = world.options.plando_connections
|
||||
if plando:
|
||||
connect_plando(world, plando)
|
||||
|
||||
@@ -278,9 +278,6 @@ class MMBN3World(World):
|
||||
self.multiworld.get_location(LocationName.Help_with_rehab, self.player).access_rule = \
|
||||
lambda state: \
|
||||
state.can_reach_region(RegionName.Beach_Overworld, self.player)
|
||||
self.multiworld.get_location(LocationName.Help_with_rehab_bonus, self.player).access_rule = \
|
||||
lambda state: \
|
||||
state.can_reach_region(RegionName.Beach_Overworld, self.player)
|
||||
self.multiworld.get_location(LocationName.Old_Master, self.player).access_rule = \
|
||||
lambda state: \
|
||||
state.can_reach_region(RegionName.ACDC_Overworld, self.player) and \
|
||||
|
||||
@@ -36,8 +36,6 @@ class MuseDashCollections:
|
||||
"Yume Ou Mono Yo Secret",
|
||||
"Echo over you... Secret",
|
||||
"Tsukuyomi Ni Naru Replaced",
|
||||
"Heart Message feat. Aoi Tokimori Secret",
|
||||
"Meow Rock feat. Chun Ge, Yuan Shen",
|
||||
]
|
||||
|
||||
song_items = SONG_DATA
|
||||
|
||||
@@ -627,18 +627,10 @@ SONG_DATA: Dict[str, SongData] = {
|
||||
"Sharp Bubbles": SongData(2900751, "83-3", "Cosmic Radio 2024", True, 7, 9, 11),
|
||||
"Replay": SongData(2900752, "83-4", "Cosmic Radio 2024", True, 5, 7, 9),
|
||||
"Cosmic Dusty Girl": SongData(2900753, "83-5", "Cosmic Radio 2024", True, 5, 7, 9),
|
||||
"Meow Rock feat. Chun Ge, Yuan Shen": SongData(2900754, "84-0", "Muse Dash・Legend", True, None, None, None),
|
||||
"Even if you make an old radio song with AI": SongData(2900755, "84-1", "Muse Dash・Legend", False, 3, 6, 8),
|
||||
"Unusual Sketchbook": SongData(2900756, "84-2", "Muse Dash・Legend", True, 6, 8, 11),
|
||||
"TransientTears": SongData(2900757, "84-3", "Muse Dash・Legend", True, 6, 8, 11),
|
||||
"SHOOTING*STAR": SongData(2900758, "84-4", "Muse Dash・Legend", False, 5, 7, 9),
|
||||
"But the Blue Bird is Already Dead": SongData(2900759, "84-5", "Muse Dash・Legend", False, 6, 8, 10),
|
||||
"Heart Message feat. Aoi Tokimori Secret": SongData(2900760, "0-57", "Default Music", True, None, 7, 10),
|
||||
"Heart Message feat. Aoi Tokimori": SongData(2900761, "0-58", "Default Music", True, 1, 3, 6),
|
||||
"Aventyr": SongData(2900762, "85-0", "Happy Otaku Pack Vol.20", True, 4, 7, 10),
|
||||
"Raintain": SongData(2900763, "85-1", "Happy Otaku Pack Vol.20", False, 6, 8, 10),
|
||||
"Piercing the Clouds and Waves": SongData(2900764, "85-2", "Happy Otaku Pack Vol.20", True, 3, 6, 8),
|
||||
"Save Yourself": SongData(2900765, "85-3", "Happy Otaku Pack Vol.20", True, 5, 7, 10),
|
||||
"Menace": SongData(2900766, "85-4", "Happy Otaku Pack Vol.20", True, 7, 9, 11),
|
||||
"Dangling": SongData(2900767, "85-5", "Happy Otaku Pack Vol.20", True, 6, 8, 10),
|
||||
"Meow Rock feat. Chun Ge, Yuan Shen": SongData(2900754, "84-0", "Muse Dash Legend", True, None, None, None),
|
||||
"Even if you make an old radio song with AI": SongData(2900755, "84-1", "Muse Dash Legend", False, 3, 6, 8),
|
||||
"Unusual Sketchbook": SongData(2900756, "84-2", "Muse Dash Legend", True, 6, 8, 11),
|
||||
"TransientTears": SongData(2900757, "84-3", "Muse Dash Legend", True, 6, 8, 11),
|
||||
"SHOOTING*STAR": SongData(2900758, "84-4", "Muse Dash Legend", False, 5, 7, 9),
|
||||
"But the Blue Bird is Already Dead": SongData(2900759, "84-5", "Muse Dash Legend", False, 6, 8, 10),
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ class NoitaWorld(World):
|
||||
web = NoitaWeb()
|
||||
|
||||
def generate_early(self) -> None:
|
||||
if not self.player_name.isascii():
|
||||
if not self.multiworld.get_player_name(self.player).isascii():
|
||||
raise Exception("Noita yaml's slot name has invalid character(s).")
|
||||
|
||||
# Returned items will be sent over to the client
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import Dict, TYPE_CHECKING
|
||||
from BaseClasses import Item, ItemClassification, Location, Region
|
||||
from . import items, locations
|
||||
|
||||
@@ -6,7 +6,7 @@ if TYPE_CHECKING:
|
||||
from . import NoitaWorld
|
||||
|
||||
|
||||
def create_event_item(player: int, name: str) -> Item:
|
||||
def create_event(player: int, name: str) -> Item:
|
||||
return items.NoitaItem(name, ItemClassification.progression, None, player)
|
||||
|
||||
|
||||
@@ -16,13 +16,13 @@ def create_location(player: int, name: str, region: Region) -> Location:
|
||||
|
||||
def create_locked_location_event(player: int, region: Region, item: str) -> Location:
|
||||
new_location = create_location(player, item, region)
|
||||
new_location.place_locked_item(create_event_item(player, item))
|
||||
new_location.place_locked_item(create_event(player, item))
|
||||
|
||||
region.locations.append(new_location)
|
||||
return new_location
|
||||
|
||||
|
||||
def create_all_events(world: "NoitaWorld", created_regions: dict[str, Region]) -> None:
|
||||
def create_all_events(world: "NoitaWorld", created_regions: Dict[str, Region]) -> None:
|
||||
for region_name, event in event_locks.items():
|
||||
region = created_regions[region_name]
|
||||
create_locked_location_event(world.player, region, event)
|
||||
@@ -31,7 +31,7 @@ def create_all_events(world: "NoitaWorld", created_regions: dict[str, Region]) -
|
||||
|
||||
|
||||
# Maps region names to event names
|
||||
event_locks: dict[str, str] = {
|
||||
event_locks: Dict[str, str] = {
|
||||
"The Work": "Victory",
|
||||
"Mines": "Portal to Holy Mountain 1",
|
||||
"Coal Pits": "Portal to Holy Mountain 2",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import itertools
|
||||
from collections import Counter
|
||||
from typing import NamedTuple, TYPE_CHECKING
|
||||
from typing import Dict, List, NamedTuple, Set, TYPE_CHECKING
|
||||
|
||||
from BaseClasses import Item, ItemClassification
|
||||
from .options import BossesAsChecks, VictoryCondition, ExtraOrbs
|
||||
@@ -27,12 +27,12 @@ def create_item(player: int, name: str) -> Item:
|
||||
return NoitaItem(name, item_data.classification, item_data.code, player)
|
||||
|
||||
|
||||
def create_fixed_item_pool() -> list[str]:
|
||||
required_items: dict[str, int] = {name: data.required_num for name, data in item_table.items()}
|
||||
def create_fixed_item_pool() -> List[str]:
|
||||
required_items: Dict[str, int] = {name: data.required_num for name, data in item_table.items()}
|
||||
return list(Counter(required_items).elements())
|
||||
|
||||
|
||||
def create_orb_items(victory_condition: VictoryCondition, extra_orbs: ExtraOrbs) -> list[str]:
|
||||
def create_orb_items(victory_condition: VictoryCondition, extra_orbs: ExtraOrbs) -> List[str]:
|
||||
orb_count = extra_orbs.value
|
||||
if victory_condition == VictoryCondition.option_pure_ending:
|
||||
orb_count = orb_count + 11
|
||||
@@ -41,15 +41,15 @@ def create_orb_items(victory_condition: VictoryCondition, extra_orbs: ExtraOrbs)
|
||||
return ["Orb" for _ in range(orb_count)]
|
||||
|
||||
|
||||
def create_spatial_awareness_item(bosses_as_checks: BossesAsChecks) -> list[str]:
|
||||
def create_spatial_awareness_item(bosses_as_checks: BossesAsChecks) -> List[str]:
|
||||
return ["Spatial Awareness Perk"] if bosses_as_checks.value >= BossesAsChecks.option_all_bosses else []
|
||||
|
||||
|
||||
def create_kantele(victory_condition: VictoryCondition) -> list[str]:
|
||||
def create_kantele(victory_condition: VictoryCondition) -> List[str]:
|
||||
return ["Kantele"] if victory_condition.value >= VictoryCondition.option_pure_ending else []
|
||||
|
||||
|
||||
def create_random_items(world: NoitaWorld, weights: dict[str, int], count: int) -> list[str]:
|
||||
def create_random_items(world: NoitaWorld, weights: Dict[str, int], count: int) -> List[str]:
|
||||
filler_pool = weights.copy()
|
||||
if not world.options.bad_effects:
|
||||
filler_pool["Trap"] = 0
|
||||
@@ -87,7 +87,7 @@ def create_all_items(world: NoitaWorld) -> None:
|
||||
|
||||
|
||||
# 110000 - 110032
|
||||
item_table: dict[str, ItemData] = {
|
||||
item_table: Dict[str, ItemData] = {
|
||||
"Trap": ItemData(110000, "Traps", ItemClassification.trap),
|
||||
"Extra Max HP": ItemData(110001, "Pickups", ItemClassification.useful),
|
||||
"Spell Refresher": ItemData(110002, "Pickups", ItemClassification.filler),
|
||||
@@ -122,7 +122,7 @@ item_table: dict[str, ItemData] = {
|
||||
"Broken Wand": ItemData(110031, "Items", ItemClassification.filler),
|
||||
}
|
||||
|
||||
shop_only_filler_weights: dict[str, int] = {
|
||||
shop_only_filler_weights: Dict[str, int] = {
|
||||
"Trap": 15,
|
||||
"Extra Max HP": 25,
|
||||
"Spell Refresher": 20,
|
||||
@@ -135,7 +135,7 @@ shop_only_filler_weights: dict[str, int] = {
|
||||
"Extra Life Perk": 10,
|
||||
}
|
||||
|
||||
filler_weights: dict[str, int] = {
|
||||
filler_weights: Dict[str, int] = {
|
||||
**shop_only_filler_weights,
|
||||
"Gold (200)": 15,
|
||||
"Gold (1000)": 6,
|
||||
@@ -152,10 +152,22 @@ filler_weights: dict[str, int] = {
|
||||
}
|
||||
|
||||
|
||||
filler_items: list[str] = list(filter(lambda item: item_table[item].classification == ItemClassification.filler,
|
||||
item_table.keys()))
|
||||
item_name_to_id: dict[str, int] = {name: data.code for name, data in item_table.items()}
|
||||
# These helper functions make the comprehensions below more readable
|
||||
def get_item_group(item_name: str) -> str:
|
||||
return item_table[item_name].group
|
||||
|
||||
item_name_groups: dict[str, set[str]] = {
|
||||
group: set(item_names) for group, item_names in itertools.groupby(item_table, lambda item: item_table[item].group)
|
||||
|
||||
def item_is_filler(item_name: str) -> bool:
|
||||
return item_table[item_name].classification == ItemClassification.filler
|
||||
|
||||
|
||||
def item_is_perk(item_name: str) -> bool:
|
||||
return item_table[item_name].group == "Perks"
|
||||
|
||||
|
||||
filler_items: List[str] = list(filter(item_is_filler, item_table.keys()))
|
||||
item_name_to_id: Dict[str, int] = {name: data.code for name, data in item_table.items()}
|
||||
|
||||
item_name_groups: Dict[str, Set[str]] = {
|
||||
group: set(item_names) for group, item_names in itertools.groupby(item_table, get_item_group)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Locations are specific points that you would obtain an item at.
|
||||
from enum import IntEnum
|
||||
from typing import NamedTuple
|
||||
from typing import Dict, NamedTuple, Optional, Set
|
||||
|
||||
from BaseClasses import Location
|
||||
|
||||
@@ -27,7 +27,7 @@ class LocationFlag(IntEnum):
|
||||
# Only the first Hidden Chest and Pedestal are mapped here, the others are created in Regions.
|
||||
# ltype key: "Chest" = Hidden Chests, "Pedestal" = Pedestals, "Boss" = Boss, "Orb" = Orb.
|
||||
# 110000-110671
|
||||
location_region_mapping: dict[str, dict[str, LocationData]] = {
|
||||
location_region_mapping: Dict[str, Dict[str, LocationData]] = {
|
||||
"Coal Pits Holy Mountain": {
|
||||
"Coal Pits Holy Mountain Shop Item 1": LocationData(110000),
|
||||
"Coal Pits Holy Mountain Shop Item 2": LocationData(110001),
|
||||
@@ -207,15 +207,15 @@ location_region_mapping: dict[str, dict[str, LocationData]] = {
|
||||
}
|
||||
|
||||
|
||||
def make_location_range(location_name: str, base_id: int, amt: int) -> dict[str, int]:
|
||||
def make_location_range(location_name: str, base_id: int, amt: int) -> Dict[str, int]:
|
||||
if amt == 1:
|
||||
return {location_name: base_id}
|
||||
return {f"{location_name} {i+1}": base_id + i for i in range(amt)}
|
||||
|
||||
|
||||
location_name_groups: dict[str, set[str]] = {"Shop": set(), "Orb": set(), "Boss": set(), "Chest": set(),
|
||||
location_name_groups: Dict[str, Set[str]] = {"Shop": set(), "Orb": set(), "Boss": set(), "Chest": set(),
|
||||
"Pedestal": set()}
|
||||
location_name_to_id: dict[str, int] = {}
|
||||
location_name_to_id: Dict[str, int] = {}
|
||||
|
||||
|
||||
for region_name, location_group in location_region_mapping.items():
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Regions are areas in your game that you travel to.
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import Dict, List, TYPE_CHECKING
|
||||
|
||||
from BaseClasses import Entrance, Region
|
||||
from . import locations
|
||||
@@ -36,21 +36,28 @@ def create_region(world: "NoitaWorld", region_name: str) -> Region:
|
||||
return new_region
|
||||
|
||||
|
||||
def create_regions(world: "NoitaWorld") -> dict[str, Region]:
|
||||
def create_regions(world: "NoitaWorld") -> Dict[str, Region]:
|
||||
return {name: create_region(world, name) for name in noita_regions}
|
||||
|
||||
|
||||
# An "Entrance" is really just a connection between two regions
|
||||
def create_entrance(player: int, source: str, destination: str, regions: Dict[str, Region]) -> Entrance:
|
||||
entrance = Entrance(player, f"From {source} To {destination}", regions[source])
|
||||
entrance.connect(regions[destination])
|
||||
return entrance
|
||||
|
||||
|
||||
# Creates connections based on our access mapping in `noita_connections`.
|
||||
def create_connections(regions: dict[str, Region]) -> None:
|
||||
def create_connections(player: int, regions: Dict[str, Region]) -> None:
|
||||
for source, destinations in noita_connections.items():
|
||||
for destination in destinations:
|
||||
regions[source].connect(regions[destination])
|
||||
new_entrances = [create_entrance(player, source, destination, regions) for destination in destinations]
|
||||
regions[source].exits = new_entrances
|
||||
|
||||
|
||||
# Creates all regions and connections. Called from NoitaWorld.
|
||||
def create_all_regions_and_connections(world: "NoitaWorld") -> None:
|
||||
created_regions = create_regions(world)
|
||||
create_connections(created_regions)
|
||||
create_connections(world.player, created_regions)
|
||||
create_all_events(world, created_regions)
|
||||
|
||||
world.multiworld.regions += created_regions.values()
|
||||
@@ -68,7 +75,7 @@ def create_all_regions_and_connections(world: "NoitaWorld") -> None:
|
||||
# - Lake is connected to The Laboratory, since the bosses are hard without specific set-ups (which means late game)
|
||||
# - Snowy Depths connects to Lava Lake orb since you need digging for it, so fairly early is acceptable
|
||||
# - Ancient Laboratory is connected to the Coal Pits, so that Ylialkemisti isn't sphere 1
|
||||
noita_connections: dict[str, list[str]] = {
|
||||
noita_connections: Dict[str, List[str]] = {
|
||||
"Menu": ["Forest"],
|
||||
"Forest": ["Mines", "Floating Island", "Desert", "Snowy Wasteland"],
|
||||
"Frozen Vault": ["The Vault"],
|
||||
@@ -110,4 +117,4 @@ noita_connections: dict[str, list[str]] = {
|
||||
###
|
||||
}
|
||||
|
||||
noita_regions: list[str] = sorted(set(noita_connections.keys()).union(*noita_connections.values()))
|
||||
noita_regions: List[str] = sorted(set(noita_connections.keys()).union(*noita_connections.values()))
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from typing import NamedTuple, TYPE_CHECKING
|
||||
from typing import List, NamedTuple, Set, TYPE_CHECKING
|
||||
|
||||
from BaseClasses import CollectionState
|
||||
from . import items, locations
|
||||
from .options import BossesAsChecks, VictoryCondition
|
||||
from worlds.generic import Rules as GenericRules
|
||||
@@ -15,7 +16,7 @@ class EntranceLock(NamedTuple):
|
||||
items_needed: int
|
||||
|
||||
|
||||
entrance_locks: list[EntranceLock] = [
|
||||
entrance_locks: List[EntranceLock] = [
|
||||
EntranceLock("Mines", "Coal Pits Holy Mountain", "Portal to Holy Mountain 1", 1),
|
||||
EntranceLock("Coal Pits", "Snowy Depths Holy Mountain", "Portal to Holy Mountain 2", 2),
|
||||
EntranceLock("Snowy Depths", "Hiisi Base Holy Mountain", "Portal to Holy Mountain 3", 3),
|
||||
@@ -26,7 +27,7 @@ entrance_locks: list[EntranceLock] = [
|
||||
]
|
||||
|
||||
|
||||
holy_mountain_regions: list[str] = [
|
||||
holy_mountain_regions: List[str] = [
|
||||
"Coal Pits Holy Mountain",
|
||||
"Snowy Depths Holy Mountain",
|
||||
"Hiisi Base Holy Mountain",
|
||||
@@ -37,7 +38,7 @@ holy_mountain_regions: list[str] = [
|
||||
]
|
||||
|
||||
|
||||
wand_tiers: list[str] = [
|
||||
wand_tiers: List[str] = [
|
||||
"Wand (Tier 1)", # Coal Pits
|
||||
"Wand (Tier 2)", # Snowy Depths
|
||||
"Wand (Tier 3)", # Hiisi Base
|
||||
@@ -47,21 +48,29 @@ wand_tiers: list[str] = [
|
||||
]
|
||||
|
||||
|
||||
items_hidden_from_shops: set[str] = {"Gold (200)", "Gold (1000)", "Potion", "Random Potion", "Secret Potion",
|
||||
items_hidden_from_shops: Set[str] = {"Gold (200)", "Gold (1000)", "Potion", "Random Potion", "Secret Potion",
|
||||
"Chaos Die", "Greed Die", "Kammi", "Refreshing Gourd", "Sädekivi", "Broken Wand",
|
||||
"Powder Pouch"}
|
||||
|
||||
perk_list: list[str] = list(filter(lambda item: items.item_table[item].group == "Perks", items.item_table.keys()))
|
||||
perk_list: List[str] = list(filter(items.item_is_perk, items.item_table.keys()))
|
||||
|
||||
|
||||
# ----------------
|
||||
# Helper Function
|
||||
# Helper Functions
|
||||
# ----------------
|
||||
|
||||
|
||||
def forbid_items_at_locations(world: "NoitaWorld", shop_locations: set[str], forbidden_items: set[str]) -> None:
|
||||
def has_perk_count(state: CollectionState, player: int, amount: int) -> bool:
|
||||
return sum(state.count(perk, player) for perk in perk_list) >= amount
|
||||
|
||||
|
||||
def has_orb_count(state: CollectionState, player: int, amount: int) -> bool:
|
||||
return state.count("Orb", player) >= amount
|
||||
|
||||
|
||||
def forbid_items_at_locations(world: "NoitaWorld", shop_locations: Set[str], forbidden_items: Set[str]) -> None:
|
||||
for shop_location in shop_locations:
|
||||
location = world.get_location(shop_location)
|
||||
location = world.multiworld.get_location(shop_location, world.player)
|
||||
GenericRules.forbid_items_for_player(location, forbidden_items, world.player)
|
||||
|
||||
|
||||
@@ -95,38 +104,38 @@ def ban_early_high_tier_wands(world: "NoitaWorld") -> None:
|
||||
|
||||
def lock_holy_mountains_into_spheres(world: "NoitaWorld") -> None:
|
||||
for lock in entrance_locks:
|
||||
location = world.get_entrance(f"{lock.source} -> {lock.destination}")
|
||||
location = world.multiworld.get_entrance(f"From {lock.source} To {lock.destination}", world.player)
|
||||
GenericRules.set_rule(location, lambda state, evt=lock.event: state.has(evt, world.player))
|
||||
|
||||
|
||||
def holy_mountain_unlock_conditions(world: "NoitaWorld") -> None:
|
||||
victory_condition = world.options.victory_condition.value
|
||||
for lock in entrance_locks:
|
||||
location = world.get_location(lock.event)
|
||||
location = world.multiworld.get_location(lock.event, world.player)
|
||||
|
||||
if victory_condition == VictoryCondition.option_greed_ending:
|
||||
location.access_rule = lambda state, items_needed=lock.items_needed: (
|
||||
state.has_group_unique("Perks", world.player, items_needed // 2)
|
||||
has_perk_count(state, world.player, items_needed//2)
|
||||
)
|
||||
elif victory_condition == VictoryCondition.option_pure_ending:
|
||||
location.access_rule = lambda state, items_needed=lock.items_needed: (
|
||||
state.has_group_unique("Perks", world.player, items_needed // 2) and
|
||||
state.has("Orb", world.player, items_needed)
|
||||
has_perk_count(state, world.player, items_needed//2) and
|
||||
has_orb_count(state, world.player, items_needed)
|
||||
)
|
||||
elif victory_condition == VictoryCondition.option_peaceful_ending:
|
||||
location.access_rule = lambda state, items_needed=lock.items_needed: (
|
||||
state.has_group_unique("Perks", world.player, items_needed // 2) and
|
||||
state.has("Orb", world.player, items_needed * 3)
|
||||
has_perk_count(state, world.player, items_needed//2) and
|
||||
has_orb_count(state, world.player, items_needed * 3)
|
||||
)
|
||||
|
||||
|
||||
def biome_unlock_conditions(world: "NoitaWorld") -> None:
|
||||
lukki_entrances = world.get_region("Lukki Lair").entrances
|
||||
magical_entrances = world.get_region("Magical Temple").entrances
|
||||
wizard_entrances = world.get_region("Wizards' Den").entrances
|
||||
lukki_entrances = world.multiworld.get_region("Lukki Lair", world.player).entrances
|
||||
magical_entrances = world.multiworld.get_region("Magical Temple", world.player).entrances
|
||||
wizard_entrances = world.multiworld.get_region("Wizards' Den", world.player).entrances
|
||||
for entrance in lukki_entrances:
|
||||
entrance.access_rule = lambda state: (
|
||||
state.has_all(("Melee Immunity Perk", "All-Seeing Eye Perk"), world.player))
|
||||
entrance.access_rule = lambda state: state.has("Melee Immunity Perk", world.player) and\
|
||||
state.has("All-Seeing Eye Perk", world.player)
|
||||
for entrance in magical_entrances:
|
||||
entrance.access_rule = lambda state: state.has("All-Seeing Eye Perk", world.player)
|
||||
for entrance in wizard_entrances:
|
||||
@@ -135,12 +144,12 @@ def biome_unlock_conditions(world: "NoitaWorld") -> None:
|
||||
|
||||
def victory_unlock_conditions(world: "NoitaWorld") -> None:
|
||||
victory_condition = world.options.victory_condition.value
|
||||
victory_location = world.get_location("Victory")
|
||||
victory_location = world.multiworld.get_location("Victory", world.player)
|
||||
|
||||
if victory_condition == VictoryCondition.option_pure_ending:
|
||||
victory_location.access_rule = lambda state: state.has("Orb", world.player, 11)
|
||||
victory_location.access_rule = lambda state: has_orb_count(state, world.player, 11)
|
||||
elif victory_condition == VictoryCondition.option_peaceful_ending:
|
||||
victory_location.access_rule = lambda state: state.has("Orb", world.player, 33)
|
||||
victory_location.access_rule = lambda state: has_orb_count(state, world.player, 33)
|
||||
|
||||
|
||||
# ----------------
|
||||
@@ -159,5 +168,5 @@ def create_all_rules(world: "NoitaWorld") -> None:
|
||||
|
||||
# Prevent the Map perk (used to find Toveri) from being on Toveri (boss)
|
||||
if world.options.bosses_as_checks.value >= BossesAsChecks.option_all_bosses:
|
||||
toveri = world.get_location("Toveri")
|
||||
toveri = world.multiworld.get_location("Toveri", world.player)
|
||||
GenericRules.forbid_items_for_player(toveri, {"Spatial Awareness Perk"}, world.player)
|
||||
|
||||
@@ -32,7 +32,7 @@ from .Cosmetics import patch_cosmetics
|
||||
|
||||
from settings import get_settings
|
||||
from BaseClasses import MultiWorld, CollectionState, Tutorial, LocationProgressType
|
||||
from Options import Range, Toggle, VerifyKeys, Accessibility, PlandoConnections, PlandoItems
|
||||
from Options import Range, Toggle, VerifyKeys, Accessibility, PlandoConnections
|
||||
from Fill import fill_restrictive, fast_fill, FillError
|
||||
from worlds.generic.Rules import exclusion_rules, add_item_rule
|
||||
from worlds.AutoWorld import World, AutoLogicRegister, WebWorld
|
||||
@@ -220,8 +220,6 @@ class OOTWorld(World):
|
||||
option_value = result.value
|
||||
elif isinstance(result, PlandoConnections):
|
||||
option_value = result.value
|
||||
elif isinstance(result, PlandoItems):
|
||||
option_value = result.value
|
||||
else:
|
||||
option_value = result.current_key
|
||||
setattr(self, option_name, option_value)
|
||||
|
||||
@@ -62,7 +62,7 @@ chunksanity_starting_chunks: typing.List[str] = [
|
||||
ItemNames.South_Of_Varrock,
|
||||
ItemNames.Central_Varrock,
|
||||
ItemNames.Varrock_Palace,
|
||||
ItemNames.Lumberyard,
|
||||
ItemNames.East_Of_Varrock,
|
||||
ItemNames.West_Varrock,
|
||||
ItemNames.Edgeville,
|
||||
ItemNames.Barbarian_Village,
|
||||
|
||||
@@ -8,9 +8,7 @@ import requests
|
||||
# The CSVs are updated at this repository to be shared between generator and client.
|
||||
data_repository_address = "https://raw.githubusercontent.com/digiholic/osrs-archipelago-logic/"
|
||||
# The Github tag of the CSVs this was generated with
|
||||
data_csv_tag = "v2.0.4"
|
||||
# If true, generate using file names in the repository
|
||||
debug = False
|
||||
data_csv_tag = "v1.5"
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
@@ -28,167 +26,98 @@ if __name__ == "__main__":
|
||||
def load_location_csv():
|
||||
this_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
with open(os.path.join(this_dir, "locations_generated.py"), 'w+') as loc_py_file:
|
||||
loc_py_file.write('"""\nThis file was auto generated by LogicCSVToPython.py\n"""\n')
|
||||
loc_py_file.write("from ..Locations import LocationRow, SkillRequirement\n")
|
||||
loc_py_file.write("\n")
|
||||
loc_py_file.write("location_rows = [\n")
|
||||
with open(os.path.join(this_dir, "locations_generated.py"), 'w+') as locPyFile:
|
||||
locPyFile.write('"""\nThis file was auto generated by LogicCSVToPython.py\n"""\n')
|
||||
locPyFile.write("from ..Locations import LocationRow, SkillRequirement\n")
|
||||
locPyFile.write("\n")
|
||||
locPyFile.write("location_rows = [\n")
|
||||
|
||||
if debug:
|
||||
with open(os.path.join(this_dir, "locations.csv"), "r") as loc_file:
|
||||
locations_reader = csv.reader(loc_file.read().splitlines())
|
||||
parse_loc_file(loc_py_file, locations_reader)
|
||||
else:
|
||||
print("Loading: " + data_repository_address + "/" + data_csv_tag + "/locations.csv")
|
||||
with requests.get(data_repository_address + "/" + data_csv_tag + "/locations.csv") as req:
|
||||
if req.status_code == 200:
|
||||
locations_reader = csv.reader(req.text.splitlines())
|
||||
parse_loc_file(loc_py_file, locations_reader)
|
||||
else:
|
||||
print(str(req.status_code) + ": " + req.reason)
|
||||
loc_py_file.write("]\n")
|
||||
with requests.get(data_repository_address + "/" + data_csv_tag + "/locations.csv") as req:
|
||||
locations_reader = csv.reader(req.text.splitlines())
|
||||
for row in locations_reader:
|
||||
row_line = "LocationRow("
|
||||
row_line += str_format(row[0])
|
||||
row_line += str_format(row[1].lower())
|
||||
|
||||
region_strings = row[2].split(", ") if row[2] else []
|
||||
row_line += f"{str_list_to_py(region_strings)}, "
|
||||
|
||||
def parse_loc_file(loc_py_file, locations_reader):
|
||||
for row in locations_reader:
|
||||
# Skip the header row, if present
|
||||
if row[0] == "Location Name":
|
||||
continue
|
||||
row_line = "LocationRow("
|
||||
row_line += str_format(row[0])
|
||||
row_line += str_format(row[1].lower())
|
||||
|
||||
region_strings = row[2].split(", ") if row[2] else []
|
||||
row_line += f"{str_list_to_py(region_strings)}, "
|
||||
|
||||
skill_strings = row[3].split(", ")
|
||||
row_line += "["
|
||||
if skill_strings:
|
||||
split_skills = [skill.split(" ") for skill in skill_strings if skill != ""]
|
||||
if split_skills:
|
||||
for split in split_skills:
|
||||
row_line += f"SkillRequirement('{split[0]}', {split[1]}), "
|
||||
row_line += "], "
|
||||
|
||||
item_strings = row[4].split(", ") if row[4] else []
|
||||
row_line += f"{str_list_to_py(item_strings)}, "
|
||||
row_line += f"{row[5]})" if row[5] != "" else "0)"
|
||||
loc_py_file.write(f"\t{row_line},\n")
|
||||
skill_strings = row[3].split(", ")
|
||||
row_line += "["
|
||||
if skill_strings:
|
||||
split_skills = [skill.split(" ") for skill in skill_strings if skill != ""]
|
||||
if split_skills:
|
||||
for split in split_skills:
|
||||
row_line += f"SkillRequirement('{split[0]}', {split[1]}), "
|
||||
row_line += "], "
|
||||
|
||||
item_strings = row[4].split(", ") if row[4] else []
|
||||
row_line += f"{str_list_to_py(item_strings)}, "
|
||||
row_line += f"{row[5]})" if row[5] != "" else "0)"
|
||||
locPyFile.write(f"\t{row_line},\n")
|
||||
locPyFile.write("]\n")
|
||||
|
||||
def load_region_csv():
|
||||
this_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
with open(os.path.join(this_dir, "regions_generated.py"), 'w+') as reg_py_file:
|
||||
reg_py_file.write('"""\nThis file was auto generated by LogicCSVToPython.py\n"""\n')
|
||||
reg_py_file.write("from ..Regions import RegionRow\n")
|
||||
reg_py_file.write("\n")
|
||||
reg_py_file.write("region_rows = [\n")
|
||||
|
||||
if debug:
|
||||
with open(os.path.join(this_dir, "regions.csv"), "r") as region_file:
|
||||
regions_reader = csv.reader(region_file.read().splitlines())
|
||||
parse_region_file(reg_py_file, regions_reader)
|
||||
else:
|
||||
print("Loading: "+ data_repository_address + "/" + data_csv_tag + "/regions.csv")
|
||||
with requests.get(data_repository_address + "/" + data_csv_tag + "/regions.csv") as req:
|
||||
if req.status_code == 200:
|
||||
regions_reader = csv.reader(req.text.splitlines())
|
||||
parse_region_file(reg_py_file, regions_reader)
|
||||
else:
|
||||
print(str(req.status_code) + ": " + req.reason)
|
||||
reg_py_file.write("]\n")
|
||||
|
||||
|
||||
def parse_region_file(reg_py_file, regions_reader):
|
||||
for row in regions_reader:
|
||||
# Skip the header row, if present
|
||||
if row[0] == "Region Name":
|
||||
continue
|
||||
|
||||
row_line = "RegionRow("
|
||||
row_line += str_format(row[0])
|
||||
row_line += str_format(row[1])
|
||||
connections = row[2]
|
||||
row_line += f"{str_list_to_py(connections.split(', '))}, "
|
||||
resources = row[3]
|
||||
row_line += f"{str_list_to_py(resources.split(', '))})"
|
||||
reg_py_file.write(f"\t{row_line},\n")
|
||||
with open(os.path.join(this_dir, "regions_generated.py"), 'w+') as regPyFile:
|
||||
regPyFile.write('"""\nThis file was auto generated by LogicCSVToPython.py\n"""\n')
|
||||
regPyFile.write("from ..Regions import RegionRow\n")
|
||||
regPyFile.write("\n")
|
||||
regPyFile.write("region_rows = [\n")
|
||||
|
||||
with requests.get(data_repository_address + "/" + data_csv_tag + "/regions.csv") as req:
|
||||
regions_reader = csv.reader(req.text.splitlines())
|
||||
for row in regions_reader:
|
||||
row_line = "RegionRow("
|
||||
row_line += str_format(row[0])
|
||||
row_line += str_format(row[1])
|
||||
connections = row[2].replace("'", "\\'")
|
||||
row_line += f"{str_list_to_py(connections.split(', '))}, "
|
||||
resources = row[3].replace("'", "\\'")
|
||||
row_line += f"{str_list_to_py(resources.split(', '))})"
|
||||
regPyFile.write(f"\t{row_line},\n")
|
||||
regPyFile.write("]\n")
|
||||
|
||||
def load_resource_csv():
|
||||
this_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
with open(os.path.join(this_dir, "resources_generated.py"), 'w+') as res_py_file:
|
||||
res_py_file.write('"""\nThis file was auto generated by LogicCSVToPython.py\n"""\n')
|
||||
res_py_file.write("from ..Regions import ResourceRow\n")
|
||||
res_py_file.write("\n")
|
||||
res_py_file.write("resource_rows = [\n")
|
||||
with open(os.path.join(this_dir, "resources_generated.py"), 'w+') as resPyFile:
|
||||
resPyFile.write('"""\nThis file was auto generated by LogicCSVToPython.py\n"""\n')
|
||||
resPyFile.write("from ..Regions import ResourceRow\n")
|
||||
resPyFile.write("\n")
|
||||
resPyFile.write("resource_rows = [\n")
|
||||
|
||||
if debug:
|
||||
with open(os.path.join(this_dir, "resources.csv"), "r") as region_file:
|
||||
regions_reader = csv.reader(region_file.read().splitlines())
|
||||
parse_resources_file(res_py_file, regions_reader)
|
||||
else:
|
||||
print("Loading: " + data_repository_address + "/" + data_csv_tag + "/resources.csv")
|
||||
with requests.get(data_repository_address + "/" + data_csv_tag + "/resources.csv") as req:
|
||||
if req.status_code == 200:
|
||||
resource_reader = csv.reader(req.text.splitlines())
|
||||
parse_resources_file(res_py_file, resource_reader)
|
||||
else:
|
||||
print(str(req.status_code) + ": " + req.reason)
|
||||
res_py_file.write("]\n")
|
||||
|
||||
|
||||
def parse_resources_file(res_py_file, resource_reader):
|
||||
for row in resource_reader:
|
||||
# Skip the header row, if present
|
||||
if row[0] == "Resource Name":
|
||||
continue
|
||||
|
||||
name = row[0].replace("'", "\\'")
|
||||
row_line = f"ResourceRow('{name}')"
|
||||
res_py_file.write(f"\t{row_line},\n")
|
||||
with requests.get(data_repository_address + "/" + data_csv_tag + "/resources.csv") as req:
|
||||
resource_reader = csv.reader(req.text.splitlines())
|
||||
for row in resource_reader:
|
||||
name = row[0].replace("'", "\\'")
|
||||
row_line = f"ResourceRow('{name}')"
|
||||
resPyFile.write(f"\t{row_line},\n")
|
||||
resPyFile.write("]\n")
|
||||
|
||||
|
||||
def load_item_csv():
|
||||
this_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
with open(os.path.join(this_dir, "items_generated.py"), 'w+') as item_py_file:
|
||||
item_py_file.write('"""\nThis file was auto generated by LogicCSVToPython.py\n"""\n')
|
||||
item_py_file.write("from BaseClasses import ItemClassification\n")
|
||||
item_py_file.write("from ..Items import ItemRow\n")
|
||||
item_py_file.write("\n")
|
||||
item_py_file.write("item_rows = [\n")
|
||||
with open(os.path.join(this_dir, "items_generated.py"), 'w+') as itemPyfile:
|
||||
itemPyfile.write('"""\nThis file was auto generated by LogicCSVToPython.py\n"""\n')
|
||||
itemPyfile.write("from BaseClasses import ItemClassification\n")
|
||||
itemPyfile.write("from ..Items import ItemRow\n")
|
||||
itemPyfile.write("\n")
|
||||
itemPyfile.write("item_rows = [\n")
|
||||
|
||||
if debug:
|
||||
with open(os.path.join(this_dir, "items.csv"), "r") as region_file:
|
||||
regions_reader = csv.reader(region_file.read().splitlines())
|
||||
parse_item_file(item_py_file, regions_reader)
|
||||
else:
|
||||
print("Loading: " + data_repository_address + "/" + data_csv_tag + "/items.csv")
|
||||
with requests.get(data_repository_address + "/" + data_csv_tag + "/items.csv") as req:
|
||||
if req.status_code == 200:
|
||||
item_reader = csv.reader(req.text.splitlines())
|
||||
parse_item_file(item_py_file, item_reader)
|
||||
else:
|
||||
print(str(req.status_code) + ": " + req.reason)
|
||||
item_py_file.write("]\n")
|
||||
with requests.get(data_repository_address + "/" + data_csv_tag + "/items.csv") as req:
|
||||
item_reader = csv.reader(req.text.splitlines())
|
||||
for row in item_reader:
|
||||
row_line = "ItemRow("
|
||||
row_line += str_format(row[0])
|
||||
row_line += f"{row[1]}, "
|
||||
|
||||
row_line += f"ItemClassification.{row[2]})"
|
||||
|
||||
def parse_item_file(item_py_file, item_reader):
|
||||
for row in item_reader:
|
||||
# Skip the header row, if present
|
||||
if row[0] == "Name":
|
||||
continue
|
||||
|
||||
row_line = "ItemRow("
|
||||
row_line += str_format(row[0])
|
||||
row_line += f"{row[1]}, "
|
||||
|
||||
row_line += f"ItemClassification.{row[2]})"
|
||||
|
||||
item_py_file.write(f"\t{row_line},\n")
|
||||
itemPyfile.write(f"\t{row_line},\n")
|
||||
itemPyfile.write("]\n")
|
||||
|
||||
|
||||
def str_format(s) -> str:
|
||||
@@ -199,7 +128,7 @@ if __name__ == "__main__":
|
||||
def str_list_to_py(str_list) -> str:
|
||||
ret_str = "["
|
||||
for s in str_list:
|
||||
ret_str += str_format(s)
|
||||
ret_str += f"'{s}', "
|
||||
ret_str += "]"
|
||||
return ret_str
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ item_rows = [
|
||||
ItemRow('Area: HAM Hideout', 1, ItemClassification.progression),
|
||||
ItemRow('Area: Lumbridge Farms', 1, ItemClassification.progression),
|
||||
ItemRow('Area: South of Varrock', 1, ItemClassification.progression),
|
||||
ItemRow('Area: Lumberyard', 1, ItemClassification.progression),
|
||||
ItemRow('Area: East Varrock', 1, ItemClassification.progression),
|
||||
ItemRow('Area: Central Varrock', 1, ItemClassification.progression),
|
||||
ItemRow('Area: Varrock Palace', 1, ItemClassification.progression),
|
||||
ItemRow('Area: West Varrock', 1, ItemClassification.progression),
|
||||
@@ -37,58 +37,7 @@ item_rows = [
|
||||
ItemRow('Progressive Armor', 6, ItemClassification.progression),
|
||||
ItemRow('Progressive Weapons', 6, ItemClassification.progression),
|
||||
ItemRow('Progressive Tools', 6, ItemClassification.useful),
|
||||
ItemRow('Progressive Ranged Weapon', 3, ItemClassification.useful),
|
||||
ItemRow('Progressive Ranged Weapons', 3, ItemClassification.useful),
|
||||
ItemRow('Progressive Ranged Armor', 3, ItemClassification.useful),
|
||||
ItemRow('Progressive Magic Spell', 2, ItemClassification.useful),
|
||||
ItemRow('An Invitation to the Gielinor Games', 1, ItemClassification.filler),
|
||||
ItemRow('Settled\'s Crossbow', 1, ItemClassification.filler),
|
||||
ItemRow('The Stone of Jas', 1, ItemClassification.filler),
|
||||
ItemRow('Nieve\'s Phone Number', 1, ItemClassification.filler),
|
||||
ItemRow('Hannanie\'s Lost Sanity', 1, ItemClassification.filler),
|
||||
ItemRow('XP Waste', 1, ItemClassification.filler),
|
||||
ItemRow('Ten Free Pulls on the Squeal of Fortune', 1, ItemClassification.filler),
|
||||
ItemRow('Project Zanaris Beta Invite', 1, ItemClassification.filler),
|
||||
ItemRow('A Funny Feeling You Would Have Been Followed', 1, ItemClassification.filler),
|
||||
ItemRow('An Ominous Prediction From Gnome Child', 1, ItemClassification.filler),
|
||||
ItemRow('A Logic Error', 1, ItemClassification.filler),
|
||||
ItemRow('The Warding Skill', 1, ItemClassification.filler),
|
||||
ItemRow('A 1/2500 Chance At Your Very Own Pet Baron Sucellus, Redeemable at your Local Duke, Some Restrictions May Apply', 1, ItemClassification.filler),
|
||||
ItemRow('A Suspicious Email From Iagex.com Asking for your Password', 1, ItemClassification.filler),
|
||||
ItemRow('A Review on that Pull Request You\'ve Been Waiting On', 1, ItemClassification.filler),
|
||||
ItemRow('Fifty Billion RS3 GP (Worthless)', 1, ItemClassification.filler),
|
||||
ItemRow('Mod Ash\'s Coffee Cup', 1, ItemClassification.filler),
|
||||
ItemRow('An Embarrasing Photo of Zammorak at the Christmas Party', 1, ItemClassification.filler),
|
||||
ItemRow('Another Bug To Report', 1, ItemClassification.filler),
|
||||
ItemRow('1-Up Mushroom', 1, ItemClassification.filler),
|
||||
ItemRow('Empty White Hallways', 1, ItemClassification.filler),
|
||||
ItemRow('Area: Menaphos', 1, ItemClassification.filler),
|
||||
ItemRow('A Ratcatchers Dialogue Rewrite', 1, ItemClassification.filler),
|
||||
ItemRow('"Nostalgia"', 1, ItemClassification.filler),
|
||||
ItemRow('A Hornless Unicorn', 1, ItemClassification.filler),
|
||||
ItemRow('The Ability To Use ::bank', 1, ItemClassification.filler),
|
||||
ItemRow('Free Haircut at the Falador Hairdresser', 1, ItemClassification.filler),
|
||||
ItemRow('Nothing Interesting Happens', 1, ItemClassification.filler),
|
||||
ItemRow('Why Fletch?', 1, ItemClassification.filler),
|
||||
ItemRow('Evolution of Combat', 1, ItemClassification.filler),
|
||||
ItemRow('Care Pack: 10,000 GP', 1, ItemClassification.useful),
|
||||
ItemRow('Care Pack: 90 Steel Nails', 1, ItemClassification.useful),
|
||||
ItemRow('Care Pack: 25 Swordfish', 1, ItemClassification.useful),
|
||||
ItemRow('Care Pack: 50 Lobsters', 1, ItemClassification.useful),
|
||||
ItemRow('Care Pack: 100 Law Runes', 1, ItemClassification.useful),
|
||||
ItemRow('Care Pack: 300 Each Elemental Rune', 1, ItemClassification.useful),
|
||||
ItemRow('Care Pack: 100 Chaos Runes', 1, ItemClassification.useful),
|
||||
ItemRow('Care Pack: 100 Death Runes', 1, ItemClassification.useful),
|
||||
ItemRow('Care Pack: 100 Oak Logs', 1, ItemClassification.useful),
|
||||
ItemRow('Care Pack: 50 Willow Logs', 1, ItemClassification.useful),
|
||||
ItemRow('Care Pack: 50 Bronze Bars', 1, ItemClassification.useful),
|
||||
ItemRow('Care Pack: 200 Iron Ore', 1, ItemClassification.useful),
|
||||
ItemRow('Care Pack: 100 Coal Ore', 1, ItemClassification.useful),
|
||||
ItemRow('Care Pack: 100 Raw Trout', 1, ItemClassification.useful),
|
||||
ItemRow('Care Pack: 200 Leather', 1, ItemClassification.useful),
|
||||
ItemRow('Care Pack: 50 Energy Potion (4)', 2, ItemClassification.useful),
|
||||
ItemRow('Care Pack: 200 Big Bones', 1, ItemClassification.useful),
|
||||
ItemRow('Care Pack: 10 Each Uncut gems', 1, ItemClassification.useful),
|
||||
ItemRow('Care Pack: 3 Rings of Forging', 1, ItemClassification.useful),
|
||||
ItemRow('Care Pack: 500 Rune Essence', 1, ItemClassification.useful),
|
||||
ItemRow('Care Pack: 200 Mind Runes', 1, ItemClassification.useful),
|
||||
ItemRow('Progressive Magic', 2, ItemClassification.useful),
|
||||
]
|
||||
|
||||
@@ -19,56 +19,37 @@ location_rows = [
|
||||
LocationRow('Quest: Witch\'s Potion', 'quest', ['Rimmington', 'Port Sarim', ], [], [], 0),
|
||||
LocationRow('Quest: The Knight\'s Sword', 'quest', ['Falador', 'Varrock Palace', 'Mudskipper Point', 'South of Varrock', 'Windmill', 'Pie Dish', 'Port Sarim', ], [SkillRequirement('Cooking', 10), SkillRequirement('Mining', 10), ], [], 0),
|
||||
LocationRow('Quest: Goblin Diplomacy', 'quest', ['Goblin Village', 'Draynor Village', 'Falador', 'South of Varrock', 'Onion', ], [], [], 0),
|
||||
LocationRow('Quest: Pirate\'s Treasure', 'quest', ['Port Sarim', 'Karamja', 'Falador', 'Central Varrock', ], [], [], 0),
|
||||
LocationRow('Quest: Pirate\'s Treasure', 'quest', ['Port Sarim', 'Karamja', 'Falador', ], [], [], 0),
|
||||
LocationRow('Quest: Rune Mysteries', 'quest', ['Lumbridge', 'Wizard Tower', 'Central Varrock', ], [], [], 0),
|
||||
LocationRow('Quest: Misthalin Mystery', 'quest', ['Lumbridge Swamp', ], [], [], 0),
|
||||
LocationRow('Quest: The Corsair Curse', 'quest', ['Rimmington', 'Falador Farms', 'Corsair Cove', ], [], [], 0),
|
||||
LocationRow('Quest: X Marks the Spot', 'quest', ['Lumbridge', 'Draynor Village', 'Port Sarim', ], [], [], 0),
|
||||
LocationRow('Quest: Below Ice Mountain', 'quest', ['Dwarven Mines', 'Dwarven Mountain Pass', 'Ice Mountain', 'Barbarian Village', 'Falador', 'Central Varrock', 'Edgeville', ], [], [], 16),
|
||||
LocationRow('Quest: Dragon Slayer', 'goal', ['Crandor', 'South of Varrock', 'Edgeville', 'Lumbridge', 'Rimmington', 'Monastery', 'Dwarven Mines', 'Port Sarim', 'Draynor Village', ], [], [], 32),
|
||||
LocationRow('Bury Some Big Bones', 'prayer', ['Big Bones', ], [SkillRequirement('Prayer', 1), ], [], 0),
|
||||
LocationRow('Activate the "Sharp Eye" Prayer', 'prayer', [], [SkillRequirement('Prayer', 8), ], [], 0),
|
||||
LocationRow('Activate the "Rock Skin" Prayer', 'prayer', [], [SkillRequirement('Prayer', 10), ], [], 0),
|
||||
LocationRow('Activate the "Protect Item" Prayer', 'prayer', [], [SkillRequirement('Prayer', 25), ], [], 2),
|
||||
LocationRow('Pray at the Edgeville Monastery', 'prayer', ['Monastery', ], [SkillRequirement('Prayer', 31), ], [], 6),
|
||||
LocationRow('Cast Bones To Bananas', 'magic', ['Nature Runes', ], [SkillRequirement('Magic', 15), ], [], 0),
|
||||
LocationRow('Cast Earth Strike', 'magic', [], [SkillRequirement('Magic', 9), ], [], 0),
|
||||
LocationRow('Cast Curse', 'magic', [], [SkillRequirement('Magic', 19), ], [], 0),
|
||||
LocationRow('Teleport to Varrock', 'magic', ['Central Varrock', 'Law Runes', ], [SkillRequirement('Magic', 25), ], [], 0),
|
||||
LocationRow('Teleport to Lumbridge', 'magic', ['Lumbridge', 'Law Runes', ], [SkillRequirement('Magic', 31), ], [], 0),
|
||||
LocationRow('Telegrab a Gold Bar from the Varrock Bank', 'magic', ['Law Runes', 'West Varrock', ], [SkillRequirement('Magic', 33), ], [], 0),
|
||||
LocationRow('Teleport to Lumbridge', 'magic', ['Lumbridge', 'Law Runes', ], [SkillRequirement('Magic', 31), ], [], 2),
|
||||
LocationRow('Teleport to Falador', 'magic', ['Falador', 'Law Runes', ], [SkillRequirement('Magic', 37), ], [], 6),
|
||||
LocationRow('Craft an Air Rune', 'runecraft', ['Rune Essence', 'Falador Farms', ], [SkillRequirement('Runecraft', 1), ], [], 0),
|
||||
LocationRow('Craft a Mind Rune', 'runecraft', ['Rune Essence', 'Goblin Village', ], [SkillRequirement('Runecraft', 2), ], [], 0),
|
||||
LocationRow('Craft a Water Rune', 'runecraft', ['Rune Essence', 'Lumbridge Swamp', ], [SkillRequirement('Runecraft', 5), ], [], 0),
|
||||
LocationRow('Craft an Earth Rune', 'runecraft', ['Rune Essence', 'Lumberyard', ], [SkillRequirement('Runecraft', 9), ], [], 0),
|
||||
LocationRow('Craft a Fire Rune', 'runecraft', ['Rune Essence', 'Al Kharid', ], [SkillRequirement('Runecraft', 14), ], [], 0),
|
||||
LocationRow('Craft a Body Rune', 'runecraft', ['Rune Essence', 'Dwarven Mountain Pass', ], [SkillRequirement('Runecraft', 20), ], [], 0),
|
||||
LocationRow('Craft runes with a Mind Core', 'runecraft', ['Camdozaal', 'Goblin Village', ], [SkillRequirement('Runecraft', 2), ], [], 0),
|
||||
LocationRow('Craft runes with a Body Core', 'runecraft', ['Camdozaal', 'Dwarven Mountain Pass', ], [SkillRequirement('Runecraft', 20), ], [], 0),
|
||||
LocationRow('Craft a Pot', 'crafting', ['Clay Ore', 'Barbarian Village', ], [SkillRequirement('Crafting', 1), ], [], 0),
|
||||
LocationRow('Craft a pair of Leather Boots', 'crafting', ['Milk', 'Al Kharid', ], [SkillRequirement('Crafting', 7), ], [], 0),
|
||||
LocationRow('Make an Unblessed Symbol', 'crafting', ['Silver Ore', 'Furnace', 'Al Kharid', 'Sheep', 'Spinning Wheel', ], [SkillRequirement('Crafting', 16), ], [], 0),
|
||||
LocationRow('Cut a Sapphire', 'crafting', ['Chisel', ], [SkillRequirement('Crafting', 20), ], [], 0),
|
||||
LocationRow('Cut an Emerald', 'crafting', ['Chisel', ], [SkillRequirement('Crafting', 27), ], [], 0),
|
||||
LocationRow('Cut a Ruby', 'crafting', ['Chisel', ], [SkillRequirement('Crafting', 34), ], [], 4),
|
||||
LocationRow('Enter the Crafting Guild', 'crafting', ['Crafting Guild', ], [SkillRequirement('Crafting', 40), ], [], 0),
|
||||
LocationRow('Cut a Diamond', 'crafting', ['Chisel', ], [SkillRequirement('Crafting', 43), ], [], 8),
|
||||
LocationRow('Mine Copper', 'crafting', ['Bronze Ores', ], [SkillRequirement('Mining', 1), ], [], 0),
|
||||
LocationRow('Mine Tin', 'crafting', ['Bronze Ores', ], [SkillRequirement('Mining', 1), ], [], 0),
|
||||
LocationRow('Mine Clay', 'crafting', ['Clay Ore', ], [SkillRequirement('Mining', 1), ], [], 0),
|
||||
LocationRow('Mine Iron', 'mining', ['Iron Ore', ], [SkillRequirement('Mining', 1), ], [], 0),
|
||||
LocationRow('Mine a Blurite Ore', 'mining', ['Mudskipper Point', 'Port Sarim', ], [SkillRequirement('Mining', 10), ], [], 0),
|
||||
LocationRow('Crush a Barronite Deposit', 'mining', ['Camdozaal', ], [SkillRequirement('Mining', 14), ], [], 0),
|
||||
LocationRow('Mine Silver', 'mining', ['Silver Ore', ], [SkillRequirement('Mining', 20), ], [], 0),
|
||||
LocationRow('Mine Coal', 'mining', ['Coal Ore', ], [SkillRequirement('Mining', 30), ], [], 2),
|
||||
LocationRow('Mine Gold', 'mining', ['Gold Ore', ], [SkillRequirement('Mining', 40), ], [], 6),
|
||||
LocationRow('Smelt a Bronze Bar', 'smithing', ['Bronze Ores', 'Furnace', ], [SkillRequirement('Smithing', 1), SkillRequirement('Mining', 1), ], [], 0),
|
||||
LocationRow('Smelt an Iron Bar', 'smithing', ['Iron Ore', 'Furnace', ], [SkillRequirement('Smithing', 15), SkillRequirement('Mining', 15), ], [], 0),
|
||||
LocationRow('Smelt a Silver Bar', 'smithing', ['Silver Ore', 'Furnace', ], [SkillRequirement('Smithing', 20), SkillRequirement('Mining', 20), ], [], 0),
|
||||
LocationRow('Smelt a Steel Bar', 'smithing', ['Coal Ore', 'Iron Ore', 'Furnace', ], [SkillRequirement('Smithing', 30), SkillRequirement('Mining', 30), ], [], 2),
|
||||
LocationRow('Smelt a Gold Bar', 'smithing', ['Gold Ore', 'Furnace', ], [SkillRequirement('Smithing', 40), SkillRequirement('Mining', 40), ], [], 6),
|
||||
LocationRow('Catch a Sardine', 'fishing', ['Shrimp Spot', ], [SkillRequirement('Fishing', 5), ], [], 0),
|
||||
LocationRow('Catch some Anchovies', 'fishing', ['Shrimp Spot', ], [SkillRequirement('Fishing', 15), ], [], 0),
|
||||
LocationRow('Catch a Trout', 'fishing', ['Fly Fishing Spot', ], [SkillRequirement('Fishing', 20), ], [], 0),
|
||||
LocationRow('Prepare a Tetra', 'fishing', ['Camdozaal', ], [SkillRequirement('Fishing', 33), SkillRequirement('Cooking', 33), ], [], 2),
|
||||
@@ -77,16 +58,13 @@ location_rows = [
|
||||
LocationRow('Bake a Redberry Pie', 'cooking', ['Redberry Bush', 'Wheat', 'Windmill', 'Pie Dish', ], [SkillRequirement('Cooking', 10), ], [], 0),
|
||||
LocationRow('Cook some Stew', 'cooking', ['Bowl', 'Meat', 'Potato', ], [SkillRequirement('Cooking', 25), ], [], 0),
|
||||
LocationRow('Bake an Apple Pie', 'cooking', ['Cooking Apple', 'Wheat', 'Windmill', 'Pie Dish', ], [SkillRequirement('Cooking', 32), ], [], 2),
|
||||
LocationRow('Enter the Cook\'s Guild', 'cooking', ['Cook\'s Guild', ], [], [], 0),
|
||||
LocationRow('Bake a Cake', 'cooking', ['Wheat', 'Windmill', 'Egg', 'Milk', 'Cake Tin', ], [SkillRequirement('Cooking', 40), ], [], 6),
|
||||
LocationRow('Bake a Meat Pizza', 'cooking', ['Wheat', 'Windmill', 'Cheese', 'Tomato', 'Meat', ], [SkillRequirement('Cooking', 45), ], [], 8),
|
||||
LocationRow('Burn a Log', 'firemaking', [], [SkillRequirement('Firemaking', 1), SkillRequirement('Woodcutting', 1), ], [], 0),
|
||||
LocationRow('Burn some Oak Logs', 'firemaking', ['Oak Tree', ], [SkillRequirement('Firemaking', 15), SkillRequirement('Woodcutting', 15), ], [], 0),
|
||||
LocationRow('Burn some Willow Logs', 'firemaking', ['Willow Tree', ], [SkillRequirement('Firemaking', 30), SkillRequirement('Woodcutting', 30), ], [], 0),
|
||||
LocationRow('Travel on a Canoe', 'woodcutting', ['Canoe Tree', ], [SkillRequirement('Woodcutting', 12), ], [], 0),
|
||||
LocationRow('Cut an Oak Log', 'woodcutting', ['Oak Tree', ], [SkillRequirement('Woodcutting', 15), ], [], 0),
|
||||
LocationRow('Cut a Willow Log', 'woodcutting', ['Willow Tree', ], [SkillRequirement('Woodcutting', 30), ], [], 0),
|
||||
LocationRow('Kill a Duck', 'combat', ['Duck', ], [SkillRequirement('Combat', 1), ], [], 0),
|
||||
LocationRow('Kill Jeff', 'combat', ['Dwarven Mountain Pass', ], [SkillRequirement('Combat', 2), ], [], 0),
|
||||
LocationRow('Kill a Goblin', 'combat', ['Goblin', ], [SkillRequirement('Combat', 2), ], [], 0),
|
||||
LocationRow('Kill a Monkey', 'combat', ['Karamja', ], [SkillRequirement('Combat', 3), ], [], 0),
|
||||
@@ -103,24 +81,19 @@ location_rows = [
|
||||
LocationRow('Kill an Ogress Shaman', 'combat', ['Corsair Cove', ], [SkillRequirement('Combat', 82), ], [], 8),
|
||||
LocationRow('Kill Obor', 'combat', ['Edgeville', ], [SkillRequirement('Combat', 106), ], [], 28),
|
||||
LocationRow('Kill Bryophyta', 'combat', ['Central Varrock', ], [SkillRequirement('Combat', 128), ], [], 28),
|
||||
LocationRow('Die', 'general', [], [], [], 0),
|
||||
LocationRow('Reach a Level 10', 'general', [], [], [], 0),
|
||||
LocationRow('Total XP 5,000', 'general', [], [], [], 0),
|
||||
LocationRow('Combat Level 5', 'general', [], [], [], 0),
|
||||
LocationRow('Total XP 10,000', 'general', [], [], [], 0),
|
||||
LocationRow('Total Level 50', 'general', [], [], [], 0),
|
||||
LocationRow('Reach a Level 20', 'general', [], [], [], 0),
|
||||
LocationRow('Total XP 25,000', 'general', [], [], [], 0),
|
||||
LocationRow('Total Level 100', 'general', [], [], [], 0),
|
||||
LocationRow('Total XP 50,000', 'general', [], [], [], 0),
|
||||
LocationRow('Combat Level 15', 'general', [], [], [], 0),
|
||||
LocationRow('Total Level 150', 'general', [], [], [], 2),
|
||||
LocationRow('Reach a Level 30', 'general', [], [], [], 2),
|
||||
LocationRow('Total XP 75,000', 'general', [], [], [], 2),
|
||||
LocationRow('Combat Level 25', 'general', [], [], [], 2),
|
||||
LocationRow('Total XP 100,000', 'general', [], [], [], 6),
|
||||
LocationRow('Total Level 200', 'general', [], [], [], 6),
|
||||
LocationRow('Reach a Level 40', 'general', [], [], [], 6),
|
||||
LocationRow('Total XP 125,000', 'general', [], [], [], 6),
|
||||
LocationRow('Combat Level 30', 'general', [], [], [], 10),
|
||||
LocationRow('Total Level 250', 'general', [], [], [], 10),
|
||||
@@ -130,28 +103,6 @@ location_rows = [
|
||||
LocationRow('Open a Simple Lockbox', 'general', ['Camdozaal', ], [], [], 0),
|
||||
LocationRow('Open an Elaborate Lockbox', 'general', ['Camdozaal', ], [], [], 0),
|
||||
LocationRow('Open an Ornate Lockbox', 'general', ['Camdozaal', ], [], [], 0),
|
||||
LocationRow('Trans your Gender', 'general', ['Makeover', ], [], [], 0),
|
||||
LocationRow('Read a Flyer from Ali the Leaflet Dropper', 'general', ['Al Kharid', 'South of Varrock', ], [], [], 0),
|
||||
LocationRow('Cry by the Members Gate to Taverley', 'general', ['Dwarven Mountain Pass', ], [], [], 0),
|
||||
LocationRow('Get Prompted to Buy Membership', 'general', [], [], [], 0),
|
||||
LocationRow('Pet the Stray Dog in Varrock', 'general', ['Central Varrock', 'West Varrock', 'South of Varrock', ], [], [], 0),
|
||||
LocationRow('Get Sent to Jail in Shantay Pass', 'general', ['Al Kharid', 'Port Sarim', ], [], [], 0),
|
||||
LocationRow('Have the Apothecary Make a Strength Potion', 'general', ['Central Varrock', 'Red Spider Eggs', 'Limpwurt Root', ], [], [], 0),
|
||||
LocationRow('Put a Whole Banana into a Bottle of Karamjan Rum', 'general', ['Karamja', ], [], [], 0),
|
||||
LocationRow('Attempt to Shear "The Thing"', 'general', ['Lumbridge Farms West', ], [], [], 0),
|
||||
LocationRow('Eat a Kebab', 'general', ['Al Kharid', ], [], [], 0),
|
||||
LocationRow('Return a Beer Glass to a Bar', 'general', ['Falador', ], [], [], 0),
|
||||
LocationRow('Enter the Varrock Bear Cage', 'general', ['Varrock Palace', ], [], [], 0),
|
||||
LocationRow('Equip a Cabbage Cape', 'general', ['Draynor Village', ], [], [], 0),
|
||||
LocationRow('Equip a Pride Scarf', 'general', ['Draynor Village', ], [], [], 0),
|
||||
LocationRow('Visit the Black Hole', 'general', ['Draynor Village', 'Dwarven Mines', ], [], [], 0),
|
||||
LocationRow('Try to Equip Goblin Mail', 'general', ['Goblin', ], [], [], 0),
|
||||
LocationRow('Equip an Orange Cape', 'general', ['Draynor Village', ], [], [], 0),
|
||||
LocationRow('Find a Needle in a Haystack', 'general', ['Haystack', ], [], [], 0),
|
||||
LocationRow('Insult the Homeless (but not Charlie he\'s cool)', 'general', ['Central Varrock', 'South of Varrock', ], [], [], 0),
|
||||
LocationRow('Dance with Party Pete', 'general', ['Falador', ], [], [], 0),
|
||||
LocationRow('Read a Newspaper', 'general', ['Central Varrock', ], [], [], 0),
|
||||
LocationRow('Add a Card to the Chronicle', 'general', ['Draynor Village', ], [], [], 0),
|
||||
LocationRow('Points: Cook\'s Assistant', 'points', [], [], [], 0),
|
||||
LocationRow('Points: Demon Slayer', 'points', [], [], [], 0),
|
||||
LocationRow('Points: The Restless Ghost', 'points', [], [], [], 0),
|
||||
|
||||
@@ -4,19 +4,19 @@ This file was auto generated by LogicCSVToPython.py
|
||||
from ..Regions import RegionRow
|
||||
|
||||
region_rows = [
|
||||
RegionRow('Lumbridge', 'Area: Lumbridge', ['Lumbridge Farms East', 'Lumbridge Farms West', 'Al Kharid', 'Lumbridge Swamp', 'HAM Hideout', 'South of Varrock', 'Barbarian Village', 'Edgeville', 'Wilderness', ], ['Mind Runes', 'Spinning Wheel', 'Furnace', 'Chisel', 'Bronze Anvil', 'Fly Fishing Spot', 'Bowl', 'Cake Tin', 'Oak Tree', 'Willow Tree', 'Canoe Tree', 'Goblin', 'Imps', 'Duck', 'Bar', ]),
|
||||
RegionRow('Lumbridge Swamp', 'Area: Lumbridge Swamp', ['Lumbridge', 'HAM Hideout', ], ['Bronze Ores', 'Coal Ore', 'Shrimp Spot', 'Meat', 'Goblin', 'Imps', 'Big Bones', 'Duck', ]),
|
||||
RegionRow('Lumbridge', 'Area: Lumbridge', ['Lumbridge Farms East', 'Lumbridge Farms West', 'Al Kharid', 'Lumbridge Swamp', 'HAM Hideout', 'South of Varrock', 'Barbarian Village', 'Edgeville', 'Wilderness', ], ['Mind Runes', 'Spinning Wheel', 'Furnace', 'Chisel', 'Bronze Anvil', 'Fly Fishing Spot', 'Bowl', 'Cake Tin', 'Oak Tree', 'Willow Tree', 'Canoe Tree', 'Goblin', 'Imps', ]),
|
||||
RegionRow('Lumbridge Swamp', 'Area: Lumbridge Swamp', ['Lumbridge', 'HAM Hideout', ], ['Bronze Ores', 'Coal Ore', 'Shrimp Spot', 'Meat', 'Goblin', 'Imps', ]),
|
||||
RegionRow('HAM Hideout', 'Area: HAM Hideout', ['Lumbridge Farms West', 'Lumbridge', 'Lumbridge Swamp', 'Draynor Village', ], ['Goblin', ]),
|
||||
RegionRow('Lumbridge Farms West', 'Area: Lumbridge Farms', ['Sourhog\'s Lair', 'HAM Hideout', 'Draynor Village', ], ['Sheep', 'Meat', 'Wheat', 'Windmill', 'Egg', 'Milk', 'Willow Tree', 'Imps', 'Potato', 'Haystack', ]),
|
||||
RegionRow('Lumbridge Farms West', 'Area: Lumbridge Farms', ['Sourhog\'s Lair', 'HAM Hideout', 'Draynor Village', ], ['Sheep', 'Meat', 'Wheat', 'Windmill', 'Egg', 'Milk', 'Willow Tree', 'Imps', 'Potato', ]),
|
||||
RegionRow('Lumbridge Farms East', 'Area: Lumbridge Farms', ['South of Varrock', 'Lumbridge', ], ['Meat', 'Egg', 'Milk', 'Willow Tree', 'Goblin', 'Imps', 'Potato', ]),
|
||||
RegionRow('Sourhog\'s Lair', 'Area: South of Varrock', ['Lumbridge Farms West', 'Draynor Manor Outskirts', ], ['', ]),
|
||||
RegionRow('South of Varrock', 'Area: South of Varrock', ['Al Kharid', 'West Varrock', 'Central Varrock', 'Lumberyard', 'Lumbridge Farms East', 'Lumbridge', 'Barbarian Village', 'Edgeville', 'Wilderness', ], ['Sheep', 'Bronze Ores', 'Iron Ore', 'Silver Ore', 'Redberry Bush', 'Meat', 'Wheat', 'Oak Tree', 'Willow Tree', 'Canoe Tree', 'Guard', 'Imps', 'Clay Ore', 'Duck', ]),
|
||||
RegionRow('Lumberyard', 'Area: Lumberyard', ['Wilderness', 'South of Varrock', 'Central Varrock', 'Varrock Palace', ], ['Guard', 'Bar', ]),
|
||||
RegionRow('Central Varrock', 'Area: Central Varrock', ['Varrock Palace', 'Lumberyard', 'South of Varrock', 'West Varrock', ], ['Mind Runes', 'Chisel', 'Anvil', 'Bowl', 'Cake Tin', 'Oak Tree', 'Barbarian', 'Guard', 'Rune Essence', 'Imps', 'Makeover', 'Bar', ]),
|
||||
RegionRow('Varrock Palace', 'Area: Varrock Palace', ['Wilderness', 'Lumberyard', 'Central Varrock', 'West Varrock', ], ['Pie Dish', 'Oak Tree', 'Zombie', 'Guard', 'Deadly Red Spider', 'Moss Giant', 'Nature Runes', 'Law Runes', 'Big Bones', 'Makeover', 'Red Spider Eggs', ]),
|
||||
RegionRow('South of Varrock', 'Area: South of Varrock', ['Al Kharid', 'West Varrock', 'Central Varrock', 'East Varrock', 'Lumbridge Farms East', 'Lumbridge', 'Barbarian Village', 'Edgeville', 'Wilderness', ], ['Sheep', 'Bronze Ores', 'Iron Ore', 'Silver Ore', 'Redberry Bush', 'Meat', 'Wheat', 'Oak Tree', 'Willow Tree', 'Canoe Tree', 'Guard', 'Imps', 'Clay Ore', ]),
|
||||
RegionRow('East Varrock', 'Area: East Varrock', ['Wilderness', 'South of Varrock', 'Central Varrock', 'Varrock Palace', ], ['Guard', ]),
|
||||
RegionRow('Central Varrock', 'Area: Central Varrock', ['Varrock Palace', 'East Varrock', 'South of Varrock', 'West Varrock', ], ['Mind Runes', 'Chisel', 'Anvil', 'Bowl', 'Cake Tin', 'Oak Tree', 'Barbarian', 'Guard', 'Rune Essence', 'Imps', ]),
|
||||
RegionRow('Varrock Palace', 'Area: Varrock Palace', ['Wilderness', 'East Varrock', 'Central Varrock', 'West Varrock', ], ['Pie Dish', 'Oak Tree', 'Zombie', 'Guard', 'Deadly Red Spider', 'Moss Giant', 'Nature Runes', 'Law Runes', ]),
|
||||
RegionRow('West Varrock', 'Area: West Varrock', ['Wilderness', 'Varrock Palace', 'South of Varrock', 'Barbarian Village', 'Edgeville', 'Cook\'s Guild', ], ['Anvil', 'Wheat', 'Oak Tree', 'Goblin', 'Guard', 'Onion', ]),
|
||||
RegionRow('Cook\'s Guild', 'Area: West Varrock*', ['West Varrock', ], ['Bowl', 'Cooking Apple', 'Pie Dish', 'Cake Tin', 'Windmill', ]),
|
||||
RegionRow('Edgeville', 'Area: Edgeville', ['Wilderness', 'West Varrock', 'Barbarian Village', 'South of Varrock', 'Lumbridge', ], ['Furnace', 'Chisel', 'Bronze Ores', 'Iron Ore', 'Coal Ore', 'Bowl', 'Meat', 'Cake Tin', 'Willow Tree', 'Canoe Tree', 'Zombie', 'Guard', 'Hill Giant', 'Nature Runes', 'Law Runes', 'Imps', 'Big Bones', 'Limpwurt Root', 'Haystack', ]),
|
||||
RegionRow('Edgeville', 'Area: Edgeville', ['Wilderness', 'West Varrock', 'Barbarian Village', 'South of Varrock', 'Lumbridge', ], ['Furnace', 'Chisel', 'Bronze Ores', 'Iron Ore', 'Coal Ore', 'Bowl', 'Meat', 'Cake Tin', 'Willow Tree', 'Canoe Tree', 'Zombie', 'Guard', 'Hill Giant', 'Nature Runes', 'Law Runes', 'Imps', ]),
|
||||
RegionRow('Barbarian Village', 'Area: Barbarian Village', ['Edgeville', 'West Varrock', 'Draynor Manor Outskirts', 'Dwarven Mountain Pass', ], ['Spinning Wheel', 'Coal Ore', 'Anvil', 'Fly Fishing Spot', 'Meat', 'Canoe Tree', 'Barbarian', 'Zombie', 'Law Runes', ]),
|
||||
RegionRow('Draynor Manor Outskirts', 'Area: Draynor Manor', ['Barbarian Village', 'Sourhog\'s Lair', 'Draynor Village', 'Falador East Outskirts', ], ['Goblin', ]),
|
||||
RegionRow('Draynor Manor', 'Area: Draynor Manor', ['Draynor Village', ], ['', ]),
|
||||
@@ -27,21 +27,21 @@ region_rows = [
|
||||
RegionRow('Ice Mountain', 'Area: Ice Mountain', ['Wilderness', 'Monastery', 'Dwarven Mines', 'Camdozaal*', ], ['', ]),
|
||||
RegionRow('Camdozaal', 'Area: Ice Mountain', ['Ice Mountain', ], ['Clay Ore', ]),
|
||||
RegionRow('Monastery', 'Area: Monastery', ['Wilderness', 'Dwarven Mountain Pass', 'Dwarven Mines', 'Ice Mountain', ], ['Sheep', ]),
|
||||
RegionRow('Falador', 'Area: Falador', ['Dwarven Mountain Pass', 'Falador Farms', 'Dwarven Mines', ], ['Furnace', 'Chisel', 'Bowl', 'Cake Tin', 'Oak Tree', 'Guard', 'Imps', 'Duck', 'Makeover', 'Bar', ]),
|
||||
RegionRow('Falador Farms', 'Area: Falador Farms', ['Falador', 'Falador East Outskirts', 'Draynor Village', 'Port Sarim', 'Rimmington', 'Crafting Guild Outskirts', ], ['Spinning Wheel', 'Meat', 'Egg', 'Milk', 'Oak Tree', 'Imps', 'Duck', ]),
|
||||
RegionRow('Falador', 'Area: Falador', ['Dwarven Mountain Pass', 'Falador Farms', 'Dwarven Mines', ], ['Furnace', 'Chisel', 'Bowl', 'Cake Tin', 'Oak Tree', 'Guard', 'Imps', ]),
|
||||
RegionRow('Falador Farms', 'Area: Falador Farms', ['Falador', 'Falador East Outskirts', 'Draynor Village', 'Port Sarim', 'Rimmington', 'Crafting Guild Outskirts', ], ['Spinning Wheel', 'Meat', 'Egg', 'Milk', 'Oak Tree', 'Imps', ]),
|
||||
RegionRow('Port Sarim', 'Area: Port Sarim', ['Falador Farms', 'Mudskipper Point', 'Rimmington', 'Karamja Docks', 'Crandor', ], ['Mind Runes', 'Shrimp Spot', 'Meat', 'Cheese', 'Tomato', 'Oak Tree', 'Willow Tree', 'Goblin', 'Potato', ]),
|
||||
RegionRow('Karamja Docks', 'Area: Mudskipper Point', ['Port Sarim', 'Karamja', ], ['', ]),
|
||||
RegionRow('Mudskipper Point', 'Area: Mudskipper Point', ['Rimmington', 'Port Sarim', ], ['Anvil', 'Ice Giant', 'Nature Runes', 'Law Runes', 'Big Bones', 'Limpwurt Root', ]),
|
||||
RegionRow('Karamja', 'Area: Karamja', ['Karamja Docks', 'Crandor', ], ['Gold Ore', 'Lobster Spot', 'Bowl', 'Cake Tin', 'Deadly Red Spider', 'Imps', 'Red Spider Eggs', ]),
|
||||
RegionRow('Crandor', 'Area: Crandor', ['Karamja', 'Port Sarim', ], ['Coal Ore', 'Gold Ore', 'Moss Giant', 'Lesser Demon', 'Nature Runes', 'Law Runes', 'Big Bones', 'Limpwurt Root', ]),
|
||||
RegionRow('Mudskipper Point', 'Area: Mudskipper Point', ['Rimmington', 'Port Sarim', ], ['Anvil', 'Ice Giant', 'Nature Runes', 'Law Runes', ]),
|
||||
RegionRow('Karamja', 'Area: Karamja', ['Karamja Docks', 'Crandor', ], ['Gold Ore', 'Lobster Spot', 'Bowl', 'Cake Tin', 'Deadly Red Spider', 'Imps', ]),
|
||||
RegionRow('Crandor', 'Area: Crandor', ['Karamja', 'Port Sarim', ], ['Coal Ore', 'Gold Ore', 'Moss Giant', 'Lesser Demon', 'Nature Runes', 'Law Runes', ]),
|
||||
RegionRow('Rimmington', 'Area: Rimmington', ['Falador Farms', 'Port Sarim', 'Mudskipper Point', 'Crafting Guild Peninsula', 'Corsair Cove', ], ['Chisel', 'Bronze Ores', 'Iron Ore', 'Gold Ore', 'Bowl', 'Cake Tin', 'Wheat', 'Oak Tree', 'Willow Tree', 'Crafting Moulds', 'Imps', 'Clay Ore', 'Onion', ]),
|
||||
RegionRow('Crafting Guild Peninsula', 'Area: Crafting Guild', ['Falador Farms', 'Rimmington', ], ['Limpwurt Root', ]),
|
||||
RegionRow('Crafting Guild Outskirts', 'Area: Crafting Guild', ['Falador Farms', 'Crafting Guild', ], ['Sheep', 'Willow Tree', 'Oak Tree', 'Makeover', ]),
|
||||
RegionRow('Crafting Guild Peninsula', 'Area: Crafting Guild', ['Falador Farms', 'Rimmington', ], ['', ]),
|
||||
RegionRow('Crafting Guild Outskirts', 'Area: Crafting Guild', ['Falador Farms', 'Crafting Guild', ], ['Sheep', 'Willow Tree', 'Oak Tree', ]),
|
||||
RegionRow('Crafting Guild', 'Area: Crafting Guild*', ['Crafting Guild', ], ['Spinning Wheel', 'Chisel', 'Silver Ore', 'Gold Ore', 'Meat', 'Milk', 'Clay Ore', ]),
|
||||
RegionRow('Draynor Village', 'Area: Draynor Village', ['Draynor Manor', 'Lumbridge Farms West', 'HAM Hideout', 'Wizard Tower', ], ['Anvil', 'Shrimp Spot', 'Wheat', 'Cheese', 'Tomato', 'Willow Tree', 'Goblin', 'Zombie', 'Nature Runes', 'Law Runes', 'Imps', ]),
|
||||
RegionRow('Wizard Tower', 'Area: Wizard Tower', ['Draynor Village', ], ['Lesser Demon', 'Rune Essence', ]),
|
||||
RegionRow('Corsair Cove', 'Area: Corsair Cove*', ['Rimmington', ], ['Anvil', 'Meat', 'Limpwurt Root', ]),
|
||||
RegionRow('Corsair Cove', 'Area: Corsair Cove*', ['Rimmington', ], ['Anvil', 'Meat', ]),
|
||||
RegionRow('Al Kharid', 'Area: Al Kharid', ['South of Varrock', 'Citharede Abbey', 'Lumbridge', 'Port Sarim', ], ['Furnace', 'Chisel', 'Bronze Ores', 'Iron Ore', 'Silver Ore', 'Coal Ore', 'Gold Ore', 'Shrimp Spot', 'Bowl', 'Cake Tin', 'Cheese', 'Crafting Moulds', 'Imps', ]),
|
||||
RegionRow('Citharede Abbey', 'Area: Citharede Abbey', ['Al Kharid', ], ['Iron Ore', 'Coal Ore', 'Anvil', 'Hill Giant', 'Nature Runes', 'Law Runes', 'Big Bones', 'Limpwurt Root', ]),
|
||||
RegionRow('Wilderness', 'Area: Wilderness', ['Lumberyard', 'Varrock Palace', 'West Varrock', 'Edgeville', 'Monastery', 'Ice Mountain', 'Goblin Village', 'South of Varrock', 'Lumbridge', ], ['Furnace', 'Chisel', 'Iron Ore', 'Coal Ore', 'Anvil', 'Meat', 'Cake Tin', 'Cheese', 'Tomato', 'Oak Tree', 'Canoe Tree', 'Zombie', 'Hill Giant', 'Deadly Red Spider', 'Moss Giant', 'Ice Giant', 'Lesser Demon', 'Nature Runes', 'Law Runes', 'Big Bones', 'Limpwurt Root', 'Bar', ]),
|
||||
RegionRow('Citharede Abbey', 'Area: Citharede Abbey', ['Al Kharid', ], ['Iron Ore', 'Coal Ore', 'Anvil', 'Hill Giant', 'Nature Runes', 'Law Runes', ]),
|
||||
RegionRow('Wilderness', 'Area: Wilderness', ['East Varrock', 'Varrock Palace', 'West Varrock', 'Edgeville', 'Monastery', 'Ice Mountain', 'Goblin Village', 'South of Varrock', 'Lumbridge', ], ['Furnace', 'Chisel', 'Iron Ore', 'Coal Ore', 'Anvil', 'Meat', 'Cake Tin', 'Cheese', 'Tomato', 'Oak Tree', 'Canoe Tree', 'Zombie', 'Hill Giant', 'Deadly Red Spider', 'Moss Giant', 'Ice Giant', 'Lesser Demon', 'Nature Runes', 'Law Runes', ]),
|
||||
]
|
||||
|
||||
@@ -51,11 +51,4 @@ resource_rows = [
|
||||
ResourceRow('Clay Ore'),
|
||||
ResourceRow('Onion'),
|
||||
ResourceRow('Potato'),
|
||||
ResourceRow('Big Bones'),
|
||||
ResourceRow('Duck'),
|
||||
ResourceRow('Makeover'),
|
||||
ResourceRow('Limpwurt Root'),
|
||||
ResourceRow('Bar'),
|
||||
ResourceRow('Haystack'),
|
||||
ResourceRow('Red Spider Eggs'),
|
||||
]
|
||||
|
||||
@@ -73,7 +73,7 @@ class ItemNames(str, Enum):
|
||||
South_Of_Varrock = "Area: South of Varrock"
|
||||
Central_Varrock = "Area: Central Varrock"
|
||||
Varrock_Palace = "Area: Varrock Palace"
|
||||
Lumberyard = "Area: Lumberyard"
|
||||
East_Of_Varrock = "Area: East Varrock"
|
||||
West_Varrock = "Area: West Varrock"
|
||||
Edgeville = "Area: Edgeville"
|
||||
Barbarian_Village = "Area: Barbarian Village"
|
||||
@@ -94,8 +94,8 @@ class ItemNames(str, Enum):
|
||||
Progressive_Weapons = "Progressive Weapons"
|
||||
Progressive_Tools = "Progressive Tools"
|
||||
Progressive_Range_Armor = "Progressive Ranged Armor"
|
||||
Progressive_Range_Weapon = "Progressive Ranged Weapon"
|
||||
Progressive_Magic = "Progressive Magic Spell"
|
||||
Progressive_Range_Weapon = "Progressive Ranged Weapons"
|
||||
Progressive_Magic = "Progressive Magic"
|
||||
Lobsters = "10 Lobsters"
|
||||
Swordfish = "5 Swordfish"
|
||||
Energy_Potions = "10 Energy Potions"
|
||||
|
||||
@@ -3,19 +3,18 @@ from dataclasses import dataclass
|
||||
from Options import Choice, Toggle, Range, PerGameCommonOptions
|
||||
|
||||
MAX_COMBAT_TASKS = 16
|
||||
|
||||
MAX_PRAYER_TASKS = 5
|
||||
MAX_MAGIC_TASKS = 7
|
||||
MAX_RUNECRAFT_TASKS = 8
|
||||
MAX_CRAFTING_TASKS = 11
|
||||
MAX_MINING_TASKS = 6
|
||||
MAX_SMITHING_TASKS = 5
|
||||
MAX_FISHING_TASKS = 6
|
||||
MAX_COOKING_TASKS = 6
|
||||
MAX_FIREMAKING_TASKS = 3
|
||||
MAX_PRAYER_TASKS = 3
|
||||
MAX_MAGIC_TASKS = 4
|
||||
MAX_RUNECRAFT_TASKS = 3
|
||||
MAX_CRAFTING_TASKS = 5
|
||||
MAX_MINING_TASKS = 5
|
||||
MAX_SMITHING_TASKS = 4
|
||||
MAX_FISHING_TASKS = 5
|
||||
MAX_COOKING_TASKS = 5
|
||||
MAX_FIREMAKING_TASKS = 2
|
||||
MAX_WOODCUTTING_TASKS = 3
|
||||
|
||||
NON_QUEST_LOCATION_COUNT = 49
|
||||
NON_QUEST_LOCATION_COUNT = 22
|
||||
|
||||
|
||||
class StartingArea(Choice):
|
||||
@@ -59,31 +58,6 @@ class ProgressiveTasks(Toggle):
|
||||
display_name = "Progressive Tasks"
|
||||
|
||||
|
||||
class EnableDuds(Toggle):
|
||||
"""
|
||||
Whether to include filler "Dud" items that serve no purpose but allow for more tasks in the pool.
|
||||
"""
|
||||
display_name = "Enable Duds"
|
||||
|
||||
|
||||
class DudCount(Range):
|
||||
"""
|
||||
How many "Dud" items to include in the pool. This setting is ignored if "Enable Duds" is not included
|
||||
"""
|
||||
display_name = "Dud Item Count"
|
||||
range_start = 0
|
||||
range_end = 30
|
||||
default = 10
|
||||
|
||||
|
||||
class EnableCarePacks(Toggle):
|
||||
"""
|
||||
Whether or not to include useful "Care Pack" items that allow you to trade over specific items.
|
||||
Note: Requires your account NOT to be an Ironman. Also, requires access to another account to trade over the items,
|
||||
or gold to purchase off of the grand exchange.
|
||||
"""
|
||||
display_name = "Enable Care Packs"
|
||||
|
||||
class MaxCombatLevel(Range):
|
||||
"""
|
||||
The highest combat level of monster to possibly be assigned as a task.
|
||||
@@ -498,9 +472,6 @@ class OSRSOptions(PerGameCommonOptions):
|
||||
starting_area: StartingArea
|
||||
brutal_grinds: BrutalGrinds
|
||||
progressive_tasks: ProgressiveTasks
|
||||
enable_duds: EnableDuds
|
||||
dud_count: DudCount
|
||||
enable_carepacks: EnableCarePacks
|
||||
max_combat_level: MaxCombatLevel
|
||||
max_combat_tasks: MaxCombatTasks
|
||||
combat_task_weight: CombatTaskWeight
|
||||
|
||||
@@ -212,14 +212,11 @@ def get_skill_rule(skill, level, player, options) -> CollectionRule:
|
||||
return lambda state: True
|
||||
|
||||
|
||||
def generate_special_rules_for(entrance, region_row, outbound_region_name, player, options, world):
|
||||
def generate_special_rules_for(entrance, region_row, outbound_region_name, player, options):
|
||||
if outbound_region_name == RegionNames.Cooks_Guild:
|
||||
add_rule(entrance, get_cooking_skill_rule(32, player, options))
|
||||
# Since there's goblins in this chunk, checking for hat access is superfluous, you'd always have it anyway
|
||||
elif outbound_region_name == RegionNames.Crafting_Guild:
|
||||
add_rule(entrance, get_crafting_skill_rule(40, player, options))
|
||||
# Literally the only brown apron access in the entirety of f2p is buying it in varrock
|
||||
add_rule(entrance, lambda state: state.can_reach_region(RegionNames.Central_Varrock, player))
|
||||
elif outbound_region_name == RegionNames.Corsair_Cove:
|
||||
# Need to be able to start Corsair Curse in addition to having the item
|
||||
add_rule(entrance, lambda state: state.can_reach(RegionNames.Falador_Farm, "Region", player))
|
||||
@@ -227,17 +224,6 @@ def generate_special_rules_for(entrance, region_row, outbound_region_name, playe
|
||||
add_rule(entrance, lambda state: state.has(ItemNames.QP_Below_Ice_Mountain, player))
|
||||
elif region_row.name == "Dwarven Mountain Pass" and outbound_region_name == "Anvil*":
|
||||
add_rule(entrance, lambda state: state.has(ItemNames.QP_Dorics_Quest, player))
|
||||
elif outbound_region_name == RegionNames.Crandor:
|
||||
add_rule(entrance, lambda state: state.can_reach_region(RegionNames.South_Of_Varrock, player))
|
||||
add_rule(entrance, lambda state: state.can_reach_region(RegionNames.Edgeville, player))
|
||||
add_rule(entrance, lambda state: state.can_reach_region(RegionNames.Lumbridge, player))
|
||||
add_rule(entrance, lambda state: state.can_reach_region(RegionNames.Rimmington, player))
|
||||
add_rule(entrance, lambda state: state.can_reach_region(RegionNames.Monastery, player))
|
||||
add_rule(entrance, lambda state: state.can_reach_region(RegionNames.Dwarven_Mines, player))
|
||||
add_rule(entrance, lambda state: state.can_reach_region(RegionNames.Port_Sarim, player))
|
||||
add_rule(entrance, lambda state: state.can_reach_region(RegionNames.Draynor_Village, player))
|
||||
add_rule(entrance, lambda state: world.quest_points(state) >= 32)
|
||||
|
||||
|
||||
# Special logic for canoes
|
||||
canoe_regions = [RegionNames.Lumbridge, RegionNames.South_Of_Varrock, RegionNames.Barbarian_Village,
|
||||
|
||||
@@ -168,7 +168,7 @@ class OSRSWorld(World):
|
||||
|
||||
item_name = self.region_rows_by_name[parsed_outbound].itemReq
|
||||
entrance.access_rule = lambda state, item_name=item_name.replace("*",""): state.has(item_name, self.player)
|
||||
generate_special_rules_for(entrance, region_row, outbound_region_name, self.player, self.options, self)
|
||||
generate_special_rules_for(entrance, region_row, outbound_region_name, self.player, self.options)
|
||||
|
||||
for resource_region in region_row.resources:
|
||||
if not resource_region:
|
||||
@@ -179,7 +179,7 @@ class OSRSWorld(World):
|
||||
entrance.connect(self.region_name_to_data[resource_region])
|
||||
else:
|
||||
entrance.connect(self.region_name_to_data[resource_region.replace('*', '')])
|
||||
generate_special_rules_for(entrance, region_row, resource_region, self.player, self.options, self)
|
||||
generate_special_rules_for(entrance, region_row, resource_region, self.player, self.options)
|
||||
|
||||
self.roll_locations()
|
||||
|
||||
@@ -195,16 +195,7 @@ class OSRSWorld(World):
|
||||
generation_is_fake = hasattr(self.multiworld, "generation_is_fake") # UT specific override
|
||||
locations_required = 0
|
||||
for item_row in item_rows:
|
||||
# If it's a filler item, set it aside for later
|
||||
if item_row.progression == ItemClassification.filler:
|
||||
continue
|
||||
|
||||
# If it starts with "Care Pack", only add it if Care Packs are enabled
|
||||
if item_row.name.startswith("Care Pack"):
|
||||
if not self.options.enable_carepacks:
|
||||
continue
|
||||
locations_required += item_row.amount
|
||||
if self.options.enable_duds: locations_required += self.options.dud_count
|
||||
|
||||
locations_added = 1 # At this point we've already added the starting area, so we start at 1 instead of 0
|
||||
|
||||
@@ -241,7 +232,6 @@ class OSRSWorld(World):
|
||||
max_amount_for_task_type = getattr(self.options, f"max_{task_type}_tasks")
|
||||
tasks_for_this_type = [task for task in self.locations_by_category[task_type]
|
||||
if self.task_within_skill_levels(task.skills)]
|
||||
max_amount_for_task_type = min(max_amount_for_task_type, len(tasks_for_this_type))
|
||||
if not self.options.progressive_tasks:
|
||||
rnd.shuffle(tasks_for_this_type)
|
||||
else:
|
||||
@@ -296,36 +286,16 @@ class OSRSWorld(World):
|
||||
self.create_and_add_location(index)
|
||||
|
||||
def create_items(self) -> None:
|
||||
filler_items = []
|
||||
for item_row in item_rows:
|
||||
if item_row.name != self.starting_area_item:
|
||||
# If it's a filler item, set it aside for later
|
||||
if item_row.progression == ItemClassification.filler:
|
||||
filler_items.append(item_row)
|
||||
continue
|
||||
|
||||
# If it starts with "Care Pack", only add it if Care Packs are enabled
|
||||
if item_row.name.startswith("Care Pack"):
|
||||
if not self.options.enable_carepacks:
|
||||
continue
|
||||
|
||||
for c in range(item_row.amount):
|
||||
item = self.create_item(item_row.name)
|
||||
self.multiworld.itempool.append(item)
|
||||
if self.options.enable_duds:
|
||||
self.random.shuffle(filler_items)
|
||||
filler_items = filler_items[0:self.options.dud_count]
|
||||
for item_row in filler_items:
|
||||
item = self.create_item(item_row.name)
|
||||
self.multiworld.itempool.append(item)
|
||||
|
||||
def get_filler_item_name(self) -> str:
|
||||
if self.options.enable_duds:
|
||||
return self.random.choice([item for item in item_rows if item.progression == ItemClassification.filler])
|
||||
else:
|
||||
return self.random.choice([ItemNames.Progressive_Weapons, ItemNames.Progressive_Magic,
|
||||
ItemNames.Progressive_Range_Weapon, ItemNames.Progressive_Armor,
|
||||
ItemNames.Progressive_Range_Armor, ItemNames.Progressive_Tools])
|
||||
return self.random.choice(
|
||||
[ItemNames.Progressive_Armor, ItemNames.Progressive_Weapons, ItemNames.Progressive_Magic,
|
||||
ItemNames.Progressive_Tools, ItemNames.Progressive_Range_Armor, ItemNames.Progressive_Range_Weapon])
|
||||
|
||||
def create_and_add_location(self, row_index) -> None:
|
||||
location_row = location_rows[row_index]
|
||||
|
||||
@@ -18,7 +18,7 @@ from .regions import create_regions
|
||||
from .options import PokemonRBOptions
|
||||
from .rom_addresses import rom_addresses
|
||||
from .text import encode_text
|
||||
from .rom import generate_output, PokemonRedProcedurePatch, PokemonBlueProcedurePatch
|
||||
from .rom import generate_output, get_base_rom_bytes, get_base_rom_path, RedDeltaPatch, BlueDeltaPatch
|
||||
from .pokemon import process_pokemon_data, process_move_data, verify_hm_moves
|
||||
from .encounters import process_pokemon_locations, process_trainer_data
|
||||
from .rules import set_rules
|
||||
@@ -33,12 +33,12 @@ class PokemonSettings(settings.Group):
|
||||
"""File names of the Pokemon Red and Blue roms"""
|
||||
description = "Pokemon Red (UE) ROM File"
|
||||
copy_to = "Pokemon Red (UE) [S][!].gb"
|
||||
md5s = [PokemonRedProcedurePatch.hash]
|
||||
md5s = [RedDeltaPatch.hash]
|
||||
|
||||
class BlueRomFile(settings.UserFilePath):
|
||||
description = "Pokemon Blue (UE) ROM File"
|
||||
copy_to = "Pokemon Blue (UE) [S][!].gb"
|
||||
md5s = [PokemonBlueProcedurePatch.hash]
|
||||
md5s = [BlueDeltaPatch.hash]
|
||||
|
||||
red_rom_file: RedRomFile = RedRomFile(RedRomFile.copy_to)
|
||||
blue_rom_file: BlueRomFile = BlueRomFile(BlueRomFile.copy_to)
|
||||
@@ -113,6 +113,16 @@ class PokemonRedBlueWorld(World):
|
||||
self.local_locs = []
|
||||
self.pc_item = None
|
||||
|
||||
@classmethod
|
||||
def stage_assert_generate(cls, multiworld: MultiWorld):
|
||||
versions = set()
|
||||
for player in multiworld.player_ids:
|
||||
if multiworld.worlds[player].game == "Pokemon Red and Blue":
|
||||
versions.add(multiworld.worlds[player].options.game_version.current_key)
|
||||
for version in versions:
|
||||
if not os.path.exists(get_base_rom_path(version)):
|
||||
raise FileNotFoundError(get_base_rom_path(version))
|
||||
|
||||
@classmethod
|
||||
def stage_generate_early(cls, multiworld: MultiWorld):
|
||||
|
||||
@@ -321,7 +331,7 @@ class PokemonRedBlueWorld(World):
|
||||
"Fuchsia Gym - Koga Prize", "Saffron Gym - Sabrina Prize",
|
||||
"Cinnabar Gym - Blaine Prize", "Viridian Gym - Giovanni Prize"
|
||||
] if self.multiworld.get_location(loc, self.player).item is None]
|
||||
state = self.multiworld.get_all_state(False, True, False)
|
||||
state = self.multiworld.get_all_state(False)
|
||||
# Give it two tries to place badges with wild Pokemon and learnsets as-is.
|
||||
# If it can't, then try with all Pokemon collected, and we'll try to fix HM move availability after.
|
||||
if attempt > 1:
|
||||
@@ -395,7 +405,7 @@ class PokemonRedBlueWorld(World):
|
||||
|
||||
# Delete evolution events for Pokémon that are not in logic in an all_state so that accessibility check does not
|
||||
# fail. Re-use test_state from previous final loop.
|
||||
all_state = self.multiworld.get_all_state(False, True, False)
|
||||
all_state = self.multiworld.get_all_state(False)
|
||||
evolutions_region = self.multiworld.get_region("Evolution", self.player)
|
||||
for location in evolutions_region.locations.copy():
|
||||
if not all_state.can_reach(location, player=self.player):
|
||||
@@ -448,7 +458,7 @@ class PokemonRedBlueWorld(World):
|
||||
|
||||
self.local_locs = locs
|
||||
|
||||
all_state = self.multiworld.get_all_state(False, True, False)
|
||||
all_state = self.multiworld.get_all_state(False)
|
||||
|
||||
reachable_mons = set()
|
||||
for mon in poke_data.pokemon_data:
|
||||
@@ -516,11 +526,6 @@ class PokemonRedBlueWorld(World):
|
||||
loc.item = None
|
||||
loc.place_locked_item(self.pc_item)
|
||||
|
||||
def get_pre_fill_items(self) -> typing.List["Item"]:
|
||||
pool = [self.create_item(mon) for mon in poke_data.pokemon_data]
|
||||
pool.append(self.pc_item)
|
||||
return pool
|
||||
|
||||
@classmethod
|
||||
def stage_post_fill(cls, multiworld):
|
||||
# Convert all but one of each instance of a wild Pokemon to useful classification.
|
||||
|
||||
@@ -1,17 +1,9 @@
|
||||
from copy import deepcopy
|
||||
import typing
|
||||
|
||||
from worlds.Files import APTokenTypes
|
||||
|
||||
from . import poke_data, logic
|
||||
from .rom_addresses import rom_addresses
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from . import PokemonRedBlueWorld
|
||||
from .rom import PokemonRedProcedurePatch, PokemonBlueProcedurePatch
|
||||
|
||||
|
||||
def set_mon_palettes(world: "PokemonRedBlueWorld", patch: "PokemonRedProcedurePatch | PokemonBlueProcedurePatch"):
|
||||
def set_mon_palettes(world, random, data):
|
||||
if world.options.randomize_pokemon_palettes == "vanilla":
|
||||
return
|
||||
pallet_map = {
|
||||
@@ -39,9 +31,12 @@ def set_mon_palettes(world: "PokemonRedBlueWorld", patch: "PokemonRedProcedurePa
|
||||
poke_data.evolves_from and poke_data.evolves_from[mon] != "Eevee"):
|
||||
pallet = palettes[-1]
|
||||
else: # completely_random or follow_evolutions and it is not an evolved form (except eeveelutions)
|
||||
pallet = world.random.choice(list(pallet_map.values()))
|
||||
pallet = random.choice(list(pallet_map.values()))
|
||||
palettes.append(pallet)
|
||||
patch.write_token(APTokenTypes.WRITE, rom_addresses["Mon_Palettes"], bytes(palettes))
|
||||
address = rom_addresses["Mon_Palettes"]
|
||||
for pallet in palettes:
|
||||
data[address] = pallet
|
||||
address += 1
|
||||
|
||||
|
||||
def choose_forced_type(chances, random):
|
||||
@@ -258,9 +253,9 @@ def process_pokemon_data(self):
|
||||
mon_data[f"start move {i}"] = learnsets[mon].pop(0)
|
||||
|
||||
if self.options.randomize_pokemon_catch_rates:
|
||||
mon_data["catch rate"] = self.random.randint(self.options.minimum_catch_rate.value, 255)
|
||||
mon_data["catch rate"] = self.random.randint(self.options.minimum_catch_rate, 255)
|
||||
else:
|
||||
mon_data["catch rate"] = max(self.options.minimum_catch_rate.value, mon_data["catch rate"])
|
||||
mon_data["catch rate"] = max(self.options.minimum_catch_rate, mon_data["catch rate"])
|
||||
|
||||
def roll_tm_compat(roll_move):
|
||||
if self.local_move_data[roll_move]["type"] in [mon_data["type1"], mon_data["type2"]]:
|
||||
@@ -400,7 +395,7 @@ def verify_hm_moves(multiworld, world, player):
|
||||
last_intervene = None
|
||||
while True:
|
||||
intervene_move = None
|
||||
test_state = multiworld.get_all_state(False, True, False)
|
||||
test_state = multiworld.get_all_state(False)
|
||||
if not logic.can_learn_hm(test_state, world, "Surf", player):
|
||||
intervene_move = "Surf"
|
||||
elif not logic.can_learn_hm(test_state, world, "Strength", player):
|
||||
|
||||
@@ -1,55 +1,5 @@
|
||||
import random
|
||||
import typing
|
||||
|
||||
from worlds.Files import APTokenTypes
|
||||
|
||||
from .rom_addresses import rom_addresses
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from .rom import PokemonBlueProcedurePatch, PokemonRedProcedurePatch
|
||||
|
||||
|
||||
layout1F = [
|
||||
[20, 22, 32, 34, 20, 25, 22, 32, 34, 20, 25, 25, 25, 22, 20, 25, 22, 2, 2, 2],
|
||||
[24, 26, 40, 1, 24, 25, 26, 62, 1, 28, 29, 29, 29, 30, 28, 29, 30, 1, 40, 2],
|
||||
[28, 30, 1, 1, 28, 29, 30, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 23],
|
||||
[23, 1, 1, 1, 1, 1, 23, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 31],
|
||||
[31, 1, 1, 1, 1, 1, 31, 32, 34, 2, 1, 1, 2, 32, 34, 32, 34, 1, 1, 23],
|
||||
[23, 1, 1, 23, 1, 1, 23, 1, 40, 23, 1, 1, 1, 1, 1, 1, 1, 1, 1, 31],
|
||||
[31, 1, 1, 31, 1, 1, 31, 1, 1, 31, 1, 1, 1, 1, 1, 1, 1, 1, 1, 23],
|
||||
[23, 1, 1, 23, 1, 1, 1, 1, 1, 2, 32, 34, 32, 34, 32, 34, 32, 34, 2, 31],
|
||||
[31, 1, 1, 31, 1, 1, 1, 1, 1, 1, 1, 23, 1, 1, 1, 23, 1, 1, 40, 23],
|
||||
[23, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 31, 1, 1, 1, 31, 1, 1, 1, 31],
|
||||
[31, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 23, 1, 1, 1, 23, 1, 1, 1, 23],
|
||||
[23, 32, 34, 32, 34, 32, 34, 32, 34, 32, 34, 31, 1, 1, 1, 31, 1, 1, 1, 31],
|
||||
[31, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 23, 1, 1, 1, 23],
|
||||
[ 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 31, 1, 1, 1, 31],
|
||||
[20, 21, 21, 21, 22, 42, 1, 1, 1, 1, 20, 21, 22, 1, 1, 1, 1, 1, 1, 23],
|
||||
[24, 25, 25, 25, 26, 1, 1, 1, 1, 1, 24, 25, 26, 1, 1, 1, 1, 1, 1, 31],
|
||||
[24, 25, 25, 25, 26, 1, 1, 62, 1, 1, 24, 25, 26, 20, 21, 21, 21, 21, 21, 22],
|
||||
[28, 29, 29, 29, 30, 78, 81, 82, 77, 78, 28, 29, 30, 28, 29, 29, 29, 29, 29, 30],
|
||||
]
|
||||
layout2F = [
|
||||
[23, 2, 32, 34, 32, 34, 32, 34, 32, 34, 32, 34, 32, 34, 32, 34, 32, 34, 32, 34],
|
||||
[31, 62, 1, 23, 1, 1, 23, 1, 1, 1, 1, 1, 23, 62, 1, 1, 1, 1, 1, 2],
|
||||
[23, 1, 1, 31, 1, 1, 31, 1, 1, 1, 1, 1, 31, 1, 1, 1, 1, 1, 1, 23],
|
||||
[31, 1, 1, 23, 1, 1, 23, 1, 1, 23, 1, 1, 23, 1, 1, 23, 23, 1, 1, 31],
|
||||
[23, 1, 1, 31, 1, 1, 31, 1, 1, 31, 2, 2, 31, 1, 1, 31, 31, 1, 1, 23],
|
||||
[31, 1, 1, 1, 1, 1, 23, 1, 1, 1, 1, 62, 23, 1, 1, 1, 1, 1, 1, 31],
|
||||
[23, 1, 1, 1, 1, 1, 31, 1, 1, 1, 1, 1, 31, 1, 1, 1, 1, 1, 1, 23],
|
||||
[31, 1, 1, 23, 1, 1, 1, 1, 1, 23, 32, 34, 32, 34, 32, 34, 1, 1, 1, 31],
|
||||
[23, 1, 1, 31, 1, 1, 1, 1, 1, 31, 1, 1, 1, 1, 1, 1, 1, 1, 1, 23],
|
||||
[31, 1, 1, 23, 1, 1, 2, 1, 1, 23, 1, 1, 1, 1, 1, 1, 1, 1, 1, 31],
|
||||
[23, 1, 1, 31, 1, 1, 2, 1, 1, 31, 1, 1, 1, 32, 34, 32, 34, 32, 34, 23],
|
||||
[31, 2, 2, 2, 1, 1, 32, 34, 32, 34, 1, 1, 1, 23, 1, 1, 1, 1, 1, 31],
|
||||
[23, 1, 1, 1, 1, 1, 23, 1, 1, 1, 1, 1, 1, 31, 1, 1, 62, 1, 1, 23],
|
||||
[31, 1, 1, 1, 1, 1, 31, 1, 1, 1, 1, 1, 1, 23, 1, 1, 1, 1, 1, 31],
|
||||
[23, 32, 34, 32, 34, 32, 34, 1, 1, 32, 34, 32, 34, 31, 1, 1, 1, 1, 1, 23],
|
||||
[31, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 31],
|
||||
[ 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 23],
|
||||
[32, 34, 32, 34, 32, 34, 32, 34, 32, 34, 32, 34, 32, 34, 32, 34, 32, 34, 2, 31]
|
||||
]
|
||||
|
||||
disallowed1F = [[2, 2], [3, 2], [1, 8], [2, 8], [7, 7], [8, 7], [10, 4], [11, 4], [11, 12],
|
||||
[11, 13], [16, 10], [17, 10], [18, 10], [16, 12], [17, 12], [18, 12]]
|
||||
disallowed2F = [[16, 2], [17, 2], [18, 2], [15, 5], [15, 6], [10, 10], [11, 10], [12, 10], [7, 14], [8, 14], [1, 15],
|
||||
@@ -57,12 +7,29 @@ disallowed2F = [[16, 2], [17, 2], [18, 2], [15, 5], [15, 6], [10, 10], [11, 10],
|
||||
[11, 1]]
|
||||
|
||||
|
||||
def randomize_rock_tunnel(patch: "PokemonRedProcedurePatch | PokemonBlueProcedurePatch", random: random.Random):
|
||||
def randomize_rock_tunnel(data, random):
|
||||
|
||||
seed = random.randint(0, 999999999999999999)
|
||||
random.seed(seed)
|
||||
|
||||
map1f = [row.copy() for row in layout1F]
|
||||
map2f = [row.copy() for row in layout2F]
|
||||
map1f = []
|
||||
map2f = []
|
||||
|
||||
address = rom_addresses["Map_Rock_Tunnel1F"]
|
||||
for y in range(0, 18):
|
||||
row = []
|
||||
for x in range(0, 20):
|
||||
row.append(data[address])
|
||||
address += 1
|
||||
map1f.append(row)
|
||||
|
||||
address = rom_addresses["Map_Rock_TunnelB1F"]
|
||||
for y in range(0, 18):
|
||||
row = []
|
||||
for x in range(0, 20):
|
||||
row.append(data[address])
|
||||
address += 1
|
||||
map2f.append(row)
|
||||
|
||||
current_map = map1f
|
||||
|
||||
@@ -338,6 +305,14 @@ def randomize_rock_tunnel(patch: "PokemonRedProcedurePatch | PokemonBlueProcedur
|
||||
current_map = map2f
|
||||
check_addable_block(map2f, disallowed2F)
|
||||
|
||||
patch.write_token(APTokenTypes.WRITE, rom_addresses["Map_Rock_Tunnel1F"], bytes([b for row in map1f for b in row]))
|
||||
patch.write_token(APTokenTypes.WRITE, rom_addresses["Map_Rock_TunnelB1F"], bytes([b for row in map2f for b in row]))
|
||||
return seed
|
||||
address = rom_addresses["Map_Rock_Tunnel1F"]
|
||||
for y in map1f:
|
||||
for x in y:
|
||||
data[address] = x
|
||||
address += 1
|
||||
address = rom_addresses["Map_Rock_TunnelB1F"]
|
||||
for y in map2f:
|
||||
for x in y:
|
||||
data[address] = x
|
||||
address += 1
|
||||
return seed
|
||||
@@ -1,66 +1,21 @@
|
||||
import os
|
||||
import pkgutil
|
||||
import typing
|
||||
|
||||
import hashlib
|
||||
import Utils
|
||||
from worlds.Files import APProcedurePatch, APTokenMixin, APTokenTypes
|
||||
|
||||
from . import poke_data
|
||||
from .items import item_table
|
||||
import bsdiff4
|
||||
import pkgutil
|
||||
from worlds.Files import APDeltaPatch
|
||||
from .text import encode_text
|
||||
from .items import item_table
|
||||
from .pokemon import set_mon_palettes
|
||||
from .regions import PokemonRBWarp, map_ids, town_map_coords
|
||||
from .rock_tunnel import randomize_rock_tunnel
|
||||
from .rom_addresses import rom_addresses
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from . import PokemonRedBlueWorld
|
||||
from .regions import PokemonRBWarp, map_ids, town_map_coords
|
||||
from . import poke_data
|
||||
|
||||
|
||||
class PokemonRedProcedurePatch(APProcedurePatch, APTokenMixin):
|
||||
game = "Pokemon Red and Blue"
|
||||
hash = "3d45c1ee9abd5738df46d2bdda8b57dc"
|
||||
patch_file_ending = ".apred"
|
||||
result_file_ending = ".gb"
|
||||
def write_quizzes(world, data, random):
|
||||
|
||||
procedure = [
|
||||
("apply_bsdiff4", ["base_patch.bsdiff4"]),
|
||||
("apply_tokens", ["token_data.bin"]),
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def get_source_data(cls) -> bytes:
|
||||
from . import PokemonRedBlueWorld
|
||||
with open(PokemonRedBlueWorld.settings.red_rom_file, "rb") as infile:
|
||||
base_rom_bytes = bytes(infile.read())
|
||||
|
||||
return base_rom_bytes
|
||||
|
||||
|
||||
class PokemonBlueProcedurePatch(APProcedurePatch, APTokenMixin):
|
||||
game = "Pokemon Red and Blue"
|
||||
hash = "50927e843568814f7ed45ec4f944bd8b"
|
||||
patch_file_ending = ".apblue"
|
||||
result_file_ending = ".gb"
|
||||
|
||||
procedure = [
|
||||
("apply_bsdiff4", ["base_patch.bsdiff4"]),
|
||||
("apply_tokens", ["token_data.bin"]),
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def get_source_data(cls) -> bytes:
|
||||
from . import PokemonRedBlueWorld
|
||||
with open(PokemonRedBlueWorld.settings.blue_rom_file, "rb") as infile:
|
||||
base_rom_bytes = bytes(infile.read())
|
||||
|
||||
return base_rom_bytes
|
||||
|
||||
|
||||
def write_quizzes(world: "PokemonRedBlueWorld", patch: PokemonBlueProcedurePatch | PokemonRedProcedurePatch):
|
||||
random = world.random
|
||||
|
||||
def get_quiz(q: int, a: int):
|
||||
def get_quiz(q, a):
|
||||
if q == 0:
|
||||
r = random.randint(0, 3)
|
||||
if r == 0:
|
||||
@@ -167,13 +122,13 @@ def write_quizzes(world: "PokemonRedBlueWorld", patch: PokemonBlueProcedurePatch
|
||||
elif q2 == 1:
|
||||
if a:
|
||||
state = random.choice(
|
||||
["Alabama", "Alaska", "Arizona", "Arkansas", "California", "Colorado", "Connecticut",
|
||||
"Delaware", "Florida", "Georgia", "Hawaii", "Idaho", "Illinois", "Indiana", "Iowa", "Kansas",
|
||||
"Kentucky", "Louisiana", "Maine", "Maryland", "Massachusetts", "Michigan", "Minnesota",
|
||||
"Mississippi", "Missouri", "Montana", "Nebraska", "Nevada", "New Jersey", "New Mexico",
|
||||
"New York", "North Carolina", "North Dakota", "Ohio", "Oklahoma", "Oregon", "Pennsylvania",
|
||||
"Rhode Island", "South Carolina", "South Dakota", "Tennessee", "Texas", "Utah", "Vermont",
|
||||
"Virginia", "Washington", "West Virginia", "Wisconsin", "Wyoming"])
|
||||
['Alabama', 'Alaska', 'Arizona', 'Arkansas', 'California', 'Colorado', 'Connecticut',
|
||||
'Delaware', 'Florida', 'Georgia', 'Hawaii', 'Idaho', 'Illinois', 'Indiana', 'Iowa', 'Kansas',
|
||||
'Kentucky', 'Louisiana', 'Maine', 'Maryland', 'Massachusetts', 'Michigan', 'Minnesota',
|
||||
'Mississippi', 'Missouri', 'Montana', 'Nebraska', 'Nevada', 'New Jersey', 'New Mexico',
|
||||
'New York', 'North Carolina', 'North Dakota', 'Ohio', 'Oklahoma', 'Oregon', 'Pennsylvania',
|
||||
'Rhode Island', 'South Carolina', 'South Dakota', 'Tennessee', 'Texas', 'Utah', 'Vermont',
|
||||
'Virginia', 'Washington', 'West Virginia', 'Wisconsin', 'Wyoming'])
|
||||
else:
|
||||
state = "New Hampshire"
|
||||
return encode_text(
|
||||
@@ -254,7 +209,7 @@ def write_quizzes(world: "PokemonRedBlueWorld", patch: PokemonBlueProcedurePatch
|
||||
return encode_text(f"{type1} deals<LINE>{eff}damage to<CONT>{type2} type?<DONE>")
|
||||
elif q == 14:
|
||||
fossil_level = world.multiworld.get_location("Fossil Level - Trainer Parties",
|
||||
world.player).party_data[0]["level"]
|
||||
world.player).party_data[0]['level']
|
||||
if not a:
|
||||
fossil_level += random.choice((-5, 5))
|
||||
return encode_text(f"Fossil #MON<LINE>revive at level<CONT>{fossil_level}?<DONE>")
|
||||
@@ -269,49 +224,46 @@ def write_quizzes(world: "PokemonRedBlueWorld", patch: PokemonBlueProcedurePatch
|
||||
return encode_text(f"According to<LINE>Monash Uni.,<CONT>{fodmap} {are_is}<CONT>considered high<CONT>in FODMAPs?<DONE>")
|
||||
|
||||
answers = [random.randint(0, 1) for _ in range(6)]
|
||||
|
||||
questions = random.sample((range(0, 16)), 6)
|
||||
question_texts: list[bytearray] = []
|
||||
|
||||
question_texts = []
|
||||
for i, question in enumerate(questions):
|
||||
question_texts.append(get_quiz(question, answers[i]))
|
||||
|
||||
for i, quiz in enumerate(["A", "B", "C", "D", "E", "F"]):
|
||||
patch.write_token(APTokenTypes.WRITE, rom_addresses[f"Quiz_Answer_{quiz}"], bytes([int(not answers[i]) << 4 | (i + 1)]))
|
||||
patch.write_token(APTokenTypes.WRITE, rom_addresses[f"Text_Quiz_{quiz}"], bytes(question_texts[i]))
|
||||
data[rom_addresses[f"Quiz_Answer_{quiz}"]] = int(not answers[i]) << 4 | (i + 1)
|
||||
write_bytes(data, question_texts[i], rom_addresses[f"Text_Quiz_{quiz}"])
|
||||
|
||||
|
||||
def generate_output(world: "PokemonRedBlueWorld", output_directory: str):
|
||||
def generate_output(world, output_directory: str):
|
||||
random = world.random
|
||||
game_version = world.options.game_version.current_key
|
||||
data = bytes(get_base_rom_bytes(game_version))
|
||||
|
||||
patch_type = PokemonBlueProcedurePatch if game_version == "blue" else PokemonRedProcedurePatch
|
||||
patch = patch_type(player=world.player, player_name=world.player_name)
|
||||
patch.write_file("base_patch.bsdiff4", pkgutil.get_data(__name__, f"basepatch_{game_version}.bsdiff4"))
|
||||
base_patch = pkgutil.get_data(__name__, f'basepatch_{game_version}.bsdiff4')
|
||||
|
||||
def write_bytes(address: int, data: typing.Sequence[int] | int):
|
||||
if isinstance(data, int):
|
||||
data = bytes([data])
|
||||
else:
|
||||
data = bytes(data)
|
||||
data = bytearray(bsdiff4.patch(data, base_patch))
|
||||
|
||||
patch.write_token(APTokenTypes.WRITE, address, data)
|
||||
basemd5 = hashlib.md5()
|
||||
basemd5.update(data)
|
||||
|
||||
pallet_connections = {entrance: world.multiworld.get_entrance(f"Pallet Town to {entrance}",
|
||||
world.player).connected_region.name
|
||||
for entrance in ["Player's House 1F", "Oak's Lab", "Rival's House"]}
|
||||
world.player).connected_region.name for
|
||||
entrance in ["Player's House 1F", "Oak's Lab",
|
||||
"Rival's House"]}
|
||||
paths = None
|
||||
|
||||
if pallet_connections["Player's House 1F"] == "Oak's Lab":
|
||||
paths = (bytes([0x00, 4, 0x80, 5, 0x40, 1, 0xE0, 1, 0xFF]), bytes([0x40, 2, 0x20, 5, 0x80, 5, 0xFF]))
|
||||
paths = ((0x00, 4, 0x80, 5, 0x40, 1, 0xE0, 1, 0xFF), (0x40, 2, 0x20, 5, 0x80, 5, 0xFF))
|
||||
elif pallet_connections["Rival's House"] == "Oak's Lab":
|
||||
paths = (bytes([0x00, 4, 0xC0, 3, 0x40, 1, 0xE0, 1, 0xFF]), bytes([0x40, 2, 0x10, 3, 0x80, 5, 0xFF]))
|
||||
|
||||
paths = ((0x00, 4, 0xC0, 3, 0x40, 1, 0xE0, 1, 0xFF), (0x40, 2, 0x10, 3, 0x80, 5, 0xFF))
|
||||
if paths:
|
||||
write_bytes(rom_addresses["Path_Pallet_Oak"], paths[0])
|
||||
write_bytes(rom_addresses["Path_Pallet_Player"], paths[1])
|
||||
|
||||
write_bytes(data, paths[0], rom_addresses["Path_Pallet_Oak"])
|
||||
write_bytes(data, paths[1], rom_addresses["Path_Pallet_Player"])
|
||||
if pallet_connections["Rival's House"] == "Player's House 1F":
|
||||
write_bytes(rom_addresses["Pallet_Fly_Coords"], [0x2F, 0xC7, 0x06, 0x0D, 0x00, 0x01])
|
||||
write_bytes(data, [0x2F, 0xC7, 0x06, 0x0D, 0x00, 0x01], rom_addresses["Pallet_Fly_Coords"])
|
||||
elif pallet_connections["Oak's Lab"] == "Player's House 1F":
|
||||
write_bytes(rom_addresses["Pallet_Fly_Coords"], [0x5F, 0xC7, 0x0C, 0x0C, 0x00, 0x00])
|
||||
write_bytes(data, [0x5F, 0xC7, 0x0C, 0x0C, 0x00, 0x00], rom_addresses["Pallet_Fly_Coords"])
|
||||
|
||||
for region in world.multiworld.get_regions(world.player):
|
||||
for entrance in region.exits:
|
||||
@@ -329,18 +281,16 @@ def generate_output(world: "PokemonRedBlueWorld", output_directory: str):
|
||||
while i > len(warp_to_ids) - 1:
|
||||
i -= len(warp_to_ids)
|
||||
connected_map_name = entrance.connected_region.name.split("-")[0]
|
||||
write_bytes(address, [
|
||||
0 if "Elevator" in connected_map_name else warp_to_ids[i],
|
||||
map_ids[connected_map_name]
|
||||
])
|
||||
data[address] = 0 if "Elevator" in connected_map_name else warp_to_ids[i]
|
||||
data[address + 1] = map_ids[connected_map_name]
|
||||
|
||||
if world.options.door_shuffle == "simple":
|
||||
for (entrance, _, _, map_coords_entries, map_name, _) in town_map_coords.values():
|
||||
destination = world.multiworld.get_entrance(entrance, world.player).connected_region.name
|
||||
(_, x, y, _, _, map_order_entry) = town_map_coords[destination]
|
||||
for map_coord_entry in map_coords_entries:
|
||||
write_bytes(rom_addresses["Town_Map_Coords"] + (map_coord_entry * 4) + 1, (y << 4) | x)
|
||||
write_bytes(rom_addresses["Town_Map_Order"] + map_order_entry, map_ids[map_name])
|
||||
data[rom_addresses["Town_Map_Coords"] + (map_coord_entry * 4) + 1] = (y << 4) | x
|
||||
data[rom_addresses["Town_Map_Order"] + map_order_entry] = map_ids[map_name]
|
||||
|
||||
if not world.options.key_items_only:
|
||||
for i, gym_leader in enumerate(("Pewter Gym - Brock TM", "Cerulean Gym - Misty TM",
|
||||
@@ -352,13 +302,13 @@ def generate_output(world: "PokemonRedBlueWorld", output_directory: str):
|
||||
try:
|
||||
tm = int(item_name[2:4])
|
||||
move = poke_data.moves[world.local_tms[tm - 1]]["id"]
|
||||
write_bytes(rom_addresses["Gym_Leader_Moves"] + (2 * i), move)
|
||||
data[rom_addresses["Gym_Leader_Moves"] + (2 * i)] = move
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
def set_trade_mon(address, loc):
|
||||
mon = world.multiworld.get_location(loc, world.player).item.name
|
||||
write_bytes(rom_addresses[address], poke_data.pokemon_data[mon]["id"])
|
||||
data[rom_addresses[address]] = poke_data.pokemon_data[mon]["id"]
|
||||
world.trade_mons[address] = mon
|
||||
|
||||
if game_version == "red":
|
||||
@@ -375,139 +325,141 @@ def generate_output(world: "PokemonRedBlueWorld", output_directory: str):
|
||||
set_trade_mon("Trade_Doris", "Cerulean Cave 1F - Wild Pokemon - 9")
|
||||
set_trade_mon("Trade_Crinkles", "Route 12 - Wild Pokemon - 4")
|
||||
|
||||
write_bytes(rom_addresses["Fly_Location"], world.fly_map_code)
|
||||
write_bytes(rom_addresses["Map_Fly_Location"], world.town_map_fly_map_code)
|
||||
data[rom_addresses['Fly_Location']] = world.fly_map_code
|
||||
data[rom_addresses['Map_Fly_Location']] = world.town_map_fly_map_code
|
||||
|
||||
if world.options.fix_combat_bugs:
|
||||
write_bytes(rom_addresses["Option_Fix_Combat_Bugs"], 1)
|
||||
write_bytes(rom_addresses["Option_Fix_Combat_Bugs_Focus_Energy"], 0x28) # jr z
|
||||
write_bytes(rom_addresses["Option_Fix_Combat_Bugs_HP_Drain_Dream_Eater"], 0x1A) # ld a, (de)
|
||||
write_bytes(rom_addresses["Option_Fix_Combat_Bugs_PP_Restore"], 0xe6) # and a, direct
|
||||
write_bytes(rom_addresses["Option_Fix_Combat_Bugs_PP_Restore"] + 1, 0b0011111)
|
||||
write_bytes(rom_addresses["Option_Fix_Combat_Bugs_Struggle"], 0xe6) # and a, direct
|
||||
write_bytes(rom_addresses["Option_Fix_Combat_Bugs_Struggle"] + 1, 0x3f)
|
||||
write_bytes(rom_addresses["Option_Fix_Combat_Bugs_Dig_Fly"], 0b10001100)
|
||||
write_bytes(rom_addresses["Option_Fix_Combat_Bugs_Heal_Effect"], 0x20) # jr nz,
|
||||
write_bytes(rom_addresses["Option_Fix_Combat_Bugs_Heal_Effect"] + 1, 5) # 5 bytes ahead
|
||||
write_bytes(rom_addresses["Option_Fix_Combat_Bugs_Heal_Stat_Modifiers"], 1)
|
||||
data[rom_addresses["Option_Fix_Combat_Bugs"]] = 1
|
||||
data[rom_addresses["Option_Fix_Combat_Bugs_Focus_Energy"]] = 0x28 # jr z
|
||||
data[rom_addresses["Option_Fix_Combat_Bugs_HP_Drain_Dream_Eater"]] = 0x1A # ld a, (de)
|
||||
data[rom_addresses["Option_Fix_Combat_Bugs_PP_Restore"]] = 0xe6 # and a, direct
|
||||
data[rom_addresses["Option_Fix_Combat_Bugs_PP_Restore"] + 1] = 0b0011111
|
||||
data[rom_addresses["Option_Fix_Combat_Bugs_Struggle"]] = 0xe6 # and a, direct
|
||||
data[rom_addresses["Option_Fix_Combat_Bugs_Struggle"] + 1] = 0x3f
|
||||
data[rom_addresses["Option_Fix_Combat_Bugs_Dig_Fly"]] = 0b10001100
|
||||
data[rom_addresses["Option_Fix_Combat_Bugs_Heal_Effect"]] = 0x20 # jr nz,
|
||||
data[rom_addresses["Option_Fix_Combat_Bugs_Heal_Effect"] + 1] = 5 # 5 bytes ahead
|
||||
data[rom_addresses["Option_Fix_Combat_Bugs_Heal_Stat_Modifiers"]] = 1
|
||||
|
||||
if world.options.poke_doll_skip == "in_logic":
|
||||
write_bytes(rom_addresses["Option_Silph_Scope_Skip"], 0x00) # nop
|
||||
write_bytes(rom_addresses["Option_Silph_Scope_Skip"] + 1, 0x00) # nop
|
||||
write_bytes(rom_addresses["Option_Silph_Scope_Skip"] + 2, 0x00) # nop
|
||||
data[rom_addresses["Option_Silph_Scope_Skip"]] = 0x00 # nop
|
||||
data[rom_addresses["Option_Silph_Scope_Skip"] + 1] = 0x00 # nop
|
||||
data[rom_addresses["Option_Silph_Scope_Skip"] + 2] = 0x00 # nop
|
||||
|
||||
if world.options.bicycle_gate_skips == "patched":
|
||||
write_bytes(rom_addresses["Option_Route_16_Gate_Fix"], 0x00) # nop
|
||||
write_bytes(rom_addresses["Option_Route_16_Gate_Fix"] + 1, 0x00) # nop
|
||||
write_bytes(rom_addresses["Option_Route_18_Gate_Fix"], 0x00) # nop
|
||||
write_bytes(rom_addresses["Option_Route_18_Gate_Fix"] + 1, 0x00) # nop
|
||||
data[rom_addresses["Option_Route_16_Gate_Fix"]] = 0x00 # nop
|
||||
data[rom_addresses["Option_Route_16_Gate_Fix"] + 1] = 0x00 # nop
|
||||
data[rom_addresses["Option_Route_18_Gate_Fix"]] = 0x00 # nop
|
||||
data[rom_addresses["Option_Route_18_Gate_Fix"] + 1] = 0x00 # nop
|
||||
|
||||
if world.options.door_shuffle:
|
||||
write_bytes(rom_addresses["Entrance_Shuffle_Fuji_Warp"], 1) # prevent warping to Fuji's House from Pokemon Tower 7F
|
||||
data[rom_addresses["Entrance_Shuffle_Fuji_Warp"]] = 1 # prevent warping to Fuji's House from Pokemon Tower 7F
|
||||
|
||||
if world.options.all_elevators_locked:
|
||||
write_bytes(rom_addresses["Option_Locked_Elevator_Celadon"], 0x20) # jr nz
|
||||
write_bytes(rom_addresses["Option_Locked_Elevator_Silph"], 0x20) # jr nz
|
||||
data[rom_addresses["Option_Locked_Elevator_Celadon"]] = 0x20 # jr nz
|
||||
data[rom_addresses["Option_Locked_Elevator_Silph"]] = 0x20 # jr nz
|
||||
|
||||
if world.options.tea:
|
||||
write_bytes(rom_addresses["Option_Tea"], 1)
|
||||
write_bytes(rom_addresses["Guard_Drink_List"], 0x54)
|
||||
write_bytes(rom_addresses["Guard_Drink_List"] + 1, 0)
|
||||
write_bytes(rom_addresses["Guard_Drink_List"] + 2, 0)
|
||||
write_bytes(rom_addresses["Text_Saffron_Gate"],
|
||||
encode_text("<LINE>Gee, I have the<CONT>worst caffeine<CONT>headache though."
|
||||
"<PARA>Oh wait there,<LINE>the road's closed.<DONE>"))
|
||||
data[rom_addresses["Option_Tea"]] = 1
|
||||
data[rom_addresses["Guard_Drink_List"]] = 0x54
|
||||
data[rom_addresses["Guard_Drink_List"] + 1] = 0
|
||||
data[rom_addresses["Guard_Drink_List"] + 2] = 0
|
||||
write_bytes(data, encode_text("<LINE>Gee, I have the<CONT>worst caffeine<CONT>headache though."
|
||||
"<PARA>Oh wait there,<LINE>the road's closed.<DONE>"),
|
||||
rom_addresses["Text_Saffron_Gate"])
|
||||
|
||||
write_bytes(rom_addresses["Tea_Key_Item_A"], 0x28) # jr .z
|
||||
write_bytes(rom_addresses["Tea_Key_Item_B"], 0x28) # jr .z
|
||||
write_bytes(rom_addresses["Tea_Key_Item_C"], 0x28) # jr .z
|
||||
data[rom_addresses["Tea_Key_Item_A"]] = 0x28 # jr .z
|
||||
data[rom_addresses["Tea_Key_Item_B"]] = 0x28 # jr .z
|
||||
data[rom_addresses["Tea_Key_Item_C"]] = 0x28 # jr .z
|
||||
|
||||
write_bytes(rom_addresses["Fossils_Needed_For_Second_Item"], world.options.second_fossil_check_condition.value)
|
||||
data[rom_addresses["Fossils_Needed_For_Second_Item"]] = (
|
||||
world.options.second_fossil_check_condition.value)
|
||||
|
||||
write_bytes(rom_addresses["Option_Lose_Money"], int(not world.options.lose_money_on_blackout.value))
|
||||
data[rom_addresses["Option_Lose_Money"]] = int(not world.options.lose_money_on_blackout.value)
|
||||
|
||||
if world.options.extra_key_items:
|
||||
write_bytes(rom_addresses["Option_Extra_Key_Items_A"], 1)
|
||||
write_bytes(rom_addresses["Option_Extra_Key_Items_B"], 1)
|
||||
write_bytes(rom_addresses["Option_Extra_Key_Items_C"], 1)
|
||||
write_bytes(rom_addresses["Option_Extra_Key_Items_D"], 1)
|
||||
write_bytes(rom_addresses["Option_Split_Card_Key"], world.options.split_card_key.value)
|
||||
write_bytes(rom_addresses["Option_Blind_Trainers"], round(world.options.blind_trainers.value * 2.55))
|
||||
write_bytes(rom_addresses["Option_Cerulean_Cave_Badges"], world.options.cerulean_cave_badges_condition.value)
|
||||
write_bytes(rom_addresses["Option_Cerulean_Cave_Key_Items"], world.options.cerulean_cave_key_items_condition.total)
|
||||
write_bytes(rom_addresses["Text_Cerulean_Cave_Badges"], encode_text(str(world.options.cerulean_cave_badges_condition.value)))
|
||||
write_bytes(rom_addresses["Text_Cerulean_Cave_Key_Items"], encode_text(str(world.options.cerulean_cave_key_items_condition.total) + " key items."))
|
||||
write_bytes(rom_addresses["Option_Encounter_Minimum_Steps"], world.options.minimum_steps_between_encounters.value)
|
||||
write_bytes(rom_addresses["Option_Route23_Badges"], world.options.victory_road_condition.value)
|
||||
write_bytes(rom_addresses["Option_Victory_Road_Badges"], world.options.route_22_gate_condition.value)
|
||||
write_bytes(rom_addresses["Option_Elite_Four_Pokedex"], world.options.elite_four_pokedex_condition.total)
|
||||
write_bytes(rom_addresses["Option_Elite_Four_Key_Items"], world.options.elite_four_key_items_condition.total)
|
||||
write_bytes(rom_addresses["Option_Elite_Four_Badges"], world.options.elite_four_badges_condition.value)
|
||||
write_bytes(rom_addresses["Text_Elite_Four_Badges"], encode_text(str(world.options.elite_four_badges_condition.value)))
|
||||
write_bytes(rom_addresses["Text_Elite_Four_Key_Items"], encode_text(str(world.options.elite_four_key_items_condition.total) + " key items, and"))
|
||||
write_bytes(rom_addresses["Text_Elite_Four_Pokedex"], encode_text(str(world.options.elite_four_pokedex_condition.total) + " #MON"))
|
||||
write_bytes(rom_addresses["Trainer_Screen_Total_Key_Items"], encode_text(str(world.total_key_items), length=2))
|
||||
data[rom_addresses['Option_Extra_Key_Items_A']] = 1
|
||||
data[rom_addresses['Option_Extra_Key_Items_B']] = 1
|
||||
data[rom_addresses['Option_Extra_Key_Items_C']] = 1
|
||||
data[rom_addresses['Option_Extra_Key_Items_D']] = 1
|
||||
data[rom_addresses["Option_Split_Card_Key"]] = world.options.split_card_key.value
|
||||
data[rom_addresses["Option_Blind_Trainers"]] = round(world.options.blind_trainers.value * 2.55)
|
||||
data[rom_addresses["Option_Cerulean_Cave_Badges"]] = world.options.cerulean_cave_badges_condition.value
|
||||
data[rom_addresses["Option_Cerulean_Cave_Key_Items"]] = world.options.cerulean_cave_key_items_condition.total
|
||||
write_bytes(data, encode_text(str(world.options.cerulean_cave_badges_condition.value)), rom_addresses["Text_Cerulean_Cave_Badges"])
|
||||
write_bytes(data, encode_text(str(world.options.cerulean_cave_key_items_condition.total) + " key items."), rom_addresses["Text_Cerulean_Cave_Key_Items"])
|
||||
data[rom_addresses['Option_Encounter_Minimum_Steps']] = world.options.minimum_steps_between_encounters.value
|
||||
data[rom_addresses['Option_Route23_Badges']] = world.options.victory_road_condition.value
|
||||
data[rom_addresses['Option_Victory_Road_Badges']] = world.options.route_22_gate_condition.value
|
||||
data[rom_addresses['Option_Elite_Four_Pokedex']] = world.options.elite_four_pokedex_condition.total
|
||||
data[rom_addresses['Option_Elite_Four_Key_Items']] = world.options.elite_four_key_items_condition.total
|
||||
data[rom_addresses['Option_Elite_Four_Badges']] = world.options.elite_four_badges_condition.value
|
||||
write_bytes(data, encode_text(str(world.options.elite_four_badges_condition.value)), rom_addresses["Text_Elite_Four_Badges"])
|
||||
write_bytes(data, encode_text(str(world.options.elite_four_key_items_condition.total) + " key items, and"), rom_addresses["Text_Elite_Four_Key_Items"])
|
||||
write_bytes(data, encode_text(str(world.options.elite_four_pokedex_condition.total) + " #MON"), rom_addresses["Text_Elite_Four_Pokedex"])
|
||||
write_bytes(data, encode_text(str(world.total_key_items), length=2), rom_addresses["Trainer_Screen_Total_Key_Items"])
|
||||
|
||||
write_bytes(rom_addresses["Option_Viridian_Gym_Badges"], world.options.viridian_gym_condition.value)
|
||||
write_bytes(rom_addresses["Option_EXP_Modifier"], world.options.exp_modifier.value)
|
||||
data[rom_addresses['Option_Viridian_Gym_Badges']] = world.options.viridian_gym_condition.value
|
||||
data[rom_addresses['Option_EXP_Modifier']] = world.options.exp_modifier.value
|
||||
if not world.options.require_item_finder:
|
||||
write_bytes(rom_addresses["Option_Itemfinder"], 0) # nop
|
||||
data[rom_addresses['Option_Itemfinder']] = 0 # nop
|
||||
if world.options.extra_strength_boulders:
|
||||
for i in range(0, 3):
|
||||
write_bytes(rom_addresses["Option_Boulders"] + (i * 3), 0x15)
|
||||
data[rom_addresses['Option_Boulders'] + (i * 3)] = 0x15
|
||||
if world.options.extra_key_items:
|
||||
for i in range(0, 4):
|
||||
write_bytes(rom_addresses["Option_Rock_Tunnel_Extra_Items"] + (i * 3), 0x15)
|
||||
data[rom_addresses['Option_Rock_Tunnel_Extra_Items'] + (i * 3)] = 0x15
|
||||
if world.options.old_man == "open_viridian_city":
|
||||
write_bytes(rom_addresses["Option_Old_Man"], 0x11)
|
||||
write_bytes(rom_addresses["Option_Old_Man_Lying"], 0x15)
|
||||
write_bytes(rom_addresses["Option_Route3_Guard_B"], world.options.route_3_condition.value)
|
||||
data[rom_addresses['Option_Old_Man']] = 0x11
|
||||
data[rom_addresses['Option_Old_Man_Lying']] = 0x15
|
||||
data[rom_addresses['Option_Route3_Guard_B']] = world.options.route_3_condition.value
|
||||
if world.options.route_3_condition == "open":
|
||||
write_bytes(rom_addresses["Option_Route3_Guard_A"], 0x11)
|
||||
data[rom_addresses['Option_Route3_Guard_A']] = 0x11
|
||||
if not world.options.robbed_house_officer:
|
||||
write_bytes(rom_addresses["Option_Trashed_House_Guard_A"], 0x15)
|
||||
write_bytes(rom_addresses["Option_Trashed_House_Guard_B"], 0x11)
|
||||
data[rom_addresses['Option_Trashed_House_Guard_A']] = 0x15
|
||||
data[rom_addresses['Option_Trashed_House_Guard_B']] = 0x11
|
||||
if world.options.require_pokedex:
|
||||
write_bytes(rom_addresses["Require_Pokedex_A"], 1)
|
||||
write_bytes(rom_addresses["Require_Pokedex_B"], 1)
|
||||
write_bytes(rom_addresses["Require_Pokedex_C"], 1)
|
||||
data[rom_addresses["Require_Pokedex_A"]] = 1
|
||||
data[rom_addresses["Require_Pokedex_B"]] = 1
|
||||
data[rom_addresses["Require_Pokedex_C"]] = 1
|
||||
else:
|
||||
write_bytes(rom_addresses["Require_Pokedex_D"], 0x18) # jr
|
||||
data[rom_addresses["Require_Pokedex_D"]] = 0x18 # jr
|
||||
if world.options.dexsanity:
|
||||
write_bytes(rom_addresses["Option_Dexsanity_A"], 1)
|
||||
write_bytes(rom_addresses["Option_Dexsanity_B"], 1)
|
||||
data[rom_addresses["Option_Dexsanity_A"]] = 1
|
||||
data[rom_addresses["Option_Dexsanity_B"]] = 1
|
||||
if world.options.all_pokemon_seen:
|
||||
write_bytes(rom_addresses["Option_Pokedex_Seen"], 1)
|
||||
data[rom_addresses["Option_Pokedex_Seen"]] = 1
|
||||
money = str(world.options.starting_money.value).zfill(6)
|
||||
write_bytes(rom_addresses["Starting_Money_High"], int(money[:2], 16))
|
||||
write_bytes(rom_addresses["Starting_Money_Middle"], int(money[2:4], 16))
|
||||
write_bytes(rom_addresses["Starting_Money_Low"], int(money[4:], 16))
|
||||
write_bytes(rom_addresses["Text_Badges_Needed_Viridian_Gym"],
|
||||
encode_text(str(world.options.viridian_gym_condition.value))[0])
|
||||
write_bytes(rom_addresses["Text_Rt23_Badges_A"],
|
||||
encode_text(str(world.options.victory_road_condition.value))[0])
|
||||
write_bytes(rom_addresses["Text_Rt23_Badges_B"],
|
||||
encode_text(str(world.options.victory_road_condition.value))[0])
|
||||
write_bytes(rom_addresses["Text_Rt23_Badges_C"],
|
||||
encode_text(str(world.options.victory_road_condition.value))[0])
|
||||
write_bytes(rom_addresses["Text_Rt23_Badges_D"],
|
||||
encode_text(str(world.options.victory_road_condition.value))[0])
|
||||
write_bytes(rom_addresses["Text_Badges_Needed"],
|
||||
encode_text(str(world.options.elite_four_badges_condition.value))[0])
|
||||
write_bytes(rom_addresses["Text_Magikarp_Salesman"],
|
||||
encode_text(" ".join(world.multiworld.get_location("Route 4 Pokemon Center - Pokemon For Sale", world.player).item.name.upper().split()[1:])))
|
||||
data[rom_addresses["Starting_Money_High"]] = int(money[:2], 16)
|
||||
data[rom_addresses["Starting_Money_Middle"]] = int(money[2:4], 16)
|
||||
data[rom_addresses["Starting_Money_Low"]] = int(money[4:], 16)
|
||||
data[rom_addresses["Text_Badges_Needed_Viridian_Gym"]] = encode_text(
|
||||
str(world.options.viridian_gym_condition.value))[0]
|
||||
data[rom_addresses["Text_Rt23_Badges_A"]] = encode_text(
|
||||
str(world.options.victory_road_condition.value))[0]
|
||||
data[rom_addresses["Text_Rt23_Badges_B"]] = encode_text(
|
||||
str(world.options.victory_road_condition.value))[0]
|
||||
data[rom_addresses["Text_Rt23_Badges_C"]] = encode_text(
|
||||
str(world.options.victory_road_condition.value))[0]
|
||||
data[rom_addresses["Text_Rt23_Badges_D"]] = encode_text(
|
||||
str(world.options.victory_road_condition.value))[0]
|
||||
data[rom_addresses["Text_Badges_Needed"]] = encode_text(
|
||||
str(world.options.elite_four_badges_condition.value))[0]
|
||||
write_bytes(data, encode_text(
|
||||
" ".join(world.multiworld.get_location("Route 4 Pokemon Center - Pokemon For Sale", world.player).item.name.upper().split()[1:])),
|
||||
rom_addresses["Text_Magikarp_Salesman"])
|
||||
|
||||
if world.options.badges_needed_for_hm_moves.value == 0:
|
||||
for hm_move in poke_data.hm_moves:
|
||||
write_bytes(rom_addresses["HM_" + hm_move + "_Badge_a"], [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])
|
||||
write_bytes(data, bytearray([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]),
|
||||
rom_addresses["HM_" + hm_move + "_Badge_a"])
|
||||
elif world.extra_badges:
|
||||
written_badges = {}
|
||||
badge_codes = {"Boulder Badge": 0x47, "Cascade Badge": 0x4F,
|
||||
"Thunder Badge": 0x57, "Rainbow Badge": 0x5F,
|
||||
"Soul Badge": 0x67, "Marsh Badge": 0x6F,
|
||||
"Volcano Badge": 0x77, "Earth Badge": 0x7F}
|
||||
for hm_move, badge in world.extra_badges.items():
|
||||
write_bytes(rom_addresses["HM_" + hm_move + "_Badge_b"], badge_codes[badge])
|
||||
data[rom_addresses["HM_" + hm_move + "_Badge_b"]] = {"Boulder Badge": 0x47, "Cascade Badge": 0x4F,
|
||||
"Thunder Badge": 0x57, "Rainbow Badge": 0x5F,
|
||||
"Soul Badge": 0x67, "Marsh Badge": 0x6F,
|
||||
"Volcano Badge": 0x77, "Earth Badge": 0x7F}[badge]
|
||||
move_text = hm_move
|
||||
if badge not in ["Marsh Badge", "Volcano Badge", "Earth Badge"]:
|
||||
move_text = ", " + move_text
|
||||
@@ -515,58 +467,62 @@ def generate_output(world: "PokemonRedBlueWorld", output_directory: str):
|
||||
if badge in written_badges:
|
||||
rom_address += len(written_badges[badge])
|
||||
move_text = ", " + move_text
|
||||
write_bytes(rom_address, encode_text(move_text.upper()))
|
||||
write_bytes(data, encode_text(move_text.upper()), rom_address)
|
||||
written_badges[badge] = move_text
|
||||
for badge in ["Marsh Badge", "Volcano Badge", "Earth Badge"]:
|
||||
if badge not in written_badges:
|
||||
write_bytes(rom_addresses["Badge_Text_" + badge.replace(" ", "_")], encode_text("Nothing"))
|
||||
write_bytes(data, encode_text("Nothing"), rom_addresses["Badge_Text_" + badge.replace(" ", "_")])
|
||||
|
||||
type_loc = rom_addresses["Type_Chart"]
|
||||
for matchup in world.type_chart:
|
||||
if matchup[2] != 10: # don't needlessly divide damage by 10 and multiply by 10
|
||||
write_bytes(type_loc, [poke_data.type_ids[matchup[0]], poke_data.type_ids[matchup[1]], matchup[2]])
|
||||
data[type_loc] = poke_data.type_ids[matchup[0]]
|
||||
data[type_loc + 1] = poke_data.type_ids[matchup[1]]
|
||||
data[type_loc + 2] = matchup[2]
|
||||
type_loc += 3
|
||||
write_bytes(type_loc, b"\xFF\xFF\xFF")
|
||||
data[type_loc] = 0xFF
|
||||
data[type_loc + 1] = 0xFF
|
||||
data[type_loc + 2] = 0xFF
|
||||
|
||||
if world.options.normalize_encounter_chances.value:
|
||||
chances = [25, 51, 77, 103, 129, 155, 180, 205, 230, 255]
|
||||
for i, chance in enumerate(chances):
|
||||
write_bytes(rom_addresses["Encounter_Chances"] + (i * 2), chance)
|
||||
data[rom_addresses['Encounter_Chances'] + (i * 2)] = chance
|
||||
|
||||
for mon, mon_data in world.local_poke_data.items():
|
||||
if mon == "Mew":
|
||||
address = rom_addresses["Base_Stats_Mew"]
|
||||
else:
|
||||
address = rom_addresses["Base_Stats"] + (28 * (mon_data["dex"] - 1))
|
||||
write_bytes(address + 1, world.local_poke_data[mon]["hp"])
|
||||
write_bytes(address + 2, world.local_poke_data[mon]["atk"])
|
||||
write_bytes(address + 3, world.local_poke_data[mon]["def"])
|
||||
write_bytes(address + 4, world.local_poke_data[mon]["spd"])
|
||||
write_bytes(address + 5, world.local_poke_data[mon]["spc"])
|
||||
write_bytes(address + 6, poke_data.type_ids[world.local_poke_data[mon]["type1"]])
|
||||
write_bytes(address + 7, poke_data.type_ids[world.local_poke_data[mon]["type2"]])
|
||||
write_bytes(address + 8, world.local_poke_data[mon]["catch rate"])
|
||||
write_bytes(address + 15, poke_data.moves[world.local_poke_data[mon]["start move 1"]]["id"])
|
||||
write_bytes(address + 16, poke_data.moves[world.local_poke_data[mon]["start move 2"]]["id"])
|
||||
write_bytes(address + 17, poke_data.moves[world.local_poke_data[mon]["start move 3"]]["id"])
|
||||
write_bytes(address + 18, poke_data.moves[world.local_poke_data[mon]["start move 4"]]["id"])
|
||||
write_bytes(address + 20, world.local_poke_data[mon]["tms"])
|
||||
data[address + 1] = world.local_poke_data[mon]["hp"]
|
||||
data[address + 2] = world.local_poke_data[mon]["atk"]
|
||||
data[address + 3] = world.local_poke_data[mon]["def"]
|
||||
data[address + 4] = world.local_poke_data[mon]["spd"]
|
||||
data[address + 5] = world.local_poke_data[mon]["spc"]
|
||||
data[address + 6] = poke_data.type_ids[world.local_poke_data[mon]["type1"]]
|
||||
data[address + 7] = poke_data.type_ids[world.local_poke_data[mon]["type2"]]
|
||||
data[address + 8] = world.local_poke_data[mon]["catch rate"]
|
||||
data[address + 15] = poke_data.moves[world.local_poke_data[mon]["start move 1"]]["id"]
|
||||
data[address + 16] = poke_data.moves[world.local_poke_data[mon]["start move 2"]]["id"]
|
||||
data[address + 17] = poke_data.moves[world.local_poke_data[mon]["start move 3"]]["id"]
|
||||
data[address + 18] = poke_data.moves[world.local_poke_data[mon]["start move 4"]]["id"]
|
||||
write_bytes(data, world.local_poke_data[mon]["tms"], address + 20)
|
||||
if mon in world.learnsets and world.learnsets[mon]:
|
||||
address = rom_addresses["Learnset_" + mon.replace(" ", "")]
|
||||
for i, move in enumerate(world.learnsets[mon]):
|
||||
write_bytes((address + 1) + i * 2, poke_data.moves[move]["id"])
|
||||
data[(address + 1) + i * 2] = poke_data.moves[move]["id"]
|
||||
|
||||
write_bytes(rom_addresses["Option_Aide_Rt2"], world.options.oaks_aide_rt_2.value)
|
||||
write_bytes(rom_addresses["Option_Aide_Rt11"], world.options.oaks_aide_rt_11.value)
|
||||
write_bytes(rom_addresses["Option_Aide_Rt15"], world.options.oaks_aide_rt_15.value)
|
||||
data[rom_addresses["Option_Aide_Rt2"]] = world.options.oaks_aide_rt_2.value
|
||||
data[rom_addresses["Option_Aide_Rt11"]] = world.options.oaks_aide_rt_11.value
|
||||
data[rom_addresses["Option_Aide_Rt15"]] = world.options.oaks_aide_rt_15.value
|
||||
|
||||
if world.options.safari_zone_normal_battles.value == 1:
|
||||
write_bytes(rom_addresses["Option_Safari_Zone_Battle_Type"], 255)
|
||||
data[rom_addresses["Option_Safari_Zone_Battle_Type"]] = 255
|
||||
|
||||
if world.options.reusable_tms.value:
|
||||
write_bytes(rom_addresses["Option_Reusable_TMs"], 0xC9)
|
||||
data[rom_addresses["Option_Reusable_TMs"]] = 0xC9
|
||||
|
||||
write_bytes(rom_addresses["Option_Always_Half_STAB"], int(not world.options.same_type_attack_bonus.value))
|
||||
data[rom_addresses["Option_Always_Half_STAB"]] = int(not world.options.same_type_attack_bonus.value)
|
||||
|
||||
if world.options.better_shops:
|
||||
inventory = ["Poke Ball", "Great Ball", "Ultra Ball"]
|
||||
@@ -575,45 +531,43 @@ def generate_output(world: "PokemonRedBlueWorld", output_directory: str):
|
||||
inventory += ["Potion", "Super Potion", "Hyper Potion", "Max Potion", "Full Restore", "Revive", "Antidote",
|
||||
"Awakening", "Burn Heal", "Ice Heal", "Paralyze Heal", "Full Heal", "Repel", "Super Repel",
|
||||
"Max Repel", "Escape Rope"]
|
||||
shop_data = [0xFE, len(inventory)]
|
||||
shop_data += [item_table[item].id - 172000000 for item in inventory]
|
||||
shop_data = bytearray([0xFE, len(inventory)])
|
||||
shop_data += bytearray([item_table[item].id - 172000000 for item in inventory])
|
||||
shop_data.append(0xFF)
|
||||
for shop in range(1, 11):
|
||||
write_bytes(rom_addresses[f"Shop{shop}"], shop_data)
|
||||
write_bytes(data, shop_data, rom_addresses[f"Shop{shop}"])
|
||||
if world.options.stonesanity:
|
||||
write_bytes(rom_addresses["Shop_Stones"], [0xFE, 1, item_table["Poke Doll"].id - 172000000, 0xFF])
|
||||
write_bytes(data, bytearray([0xFE, 1, item_table["Poke Doll"].id - 172000000, 0xFF]), rom_addresses[f"Shop_Stones"])
|
||||
|
||||
price = str(world.options.master_ball_price.value).zfill(6)
|
||||
price = [int(price[:2], 16), int(price[2:4], 16), int(price[4:], 16)]
|
||||
write_bytes(rom_addresses["Price_Master_Ball"], price) # Money values in Red and Blue are weird
|
||||
price = bytearray([int(price[:2], 16), int(price[2:4], 16), int(price[4:], 16)])
|
||||
write_bytes(data, price, rom_addresses["Price_Master_Ball"]) # Money values in Red and Blue are weird
|
||||
|
||||
from collections import Counter
|
||||
start_inventory = Counter(item.code for item in reversed(world.multiworld.precollected_items[world.player]))
|
||||
for item, value in start_inventory.items():
|
||||
write_bytes(rom_addresses["Start_Inventory"] + item - 172000000, min(value, 255))
|
||||
for item in reversed(world.multiworld.precollected_items[world.player]):
|
||||
if data[rom_addresses["Start_Inventory"] + item.code - 172000000] < 255:
|
||||
data[rom_addresses["Start_Inventory"] + item.code - 172000000] += 1
|
||||
|
||||
set_mon_palettes(world, patch)
|
||||
set_mon_palettes(world, random, data)
|
||||
|
||||
for move_data in world.local_move_data.values():
|
||||
if move_data["id"] == 0:
|
||||
continue
|
||||
address = rom_addresses["Move_Data"] + ((move_data["id"] - 1) * 6)
|
||||
write_bytes(address, [move_data["id"], move_data["effect"], move_data["power"],
|
||||
poke_data.type_ids[move_data["type"]], round(move_data["accuracy"] * 2.55),
|
||||
move_data["pp"]])
|
||||
write_bytes(data, bytearray([move_data["id"], move_data["effect"], move_data["power"],
|
||||
poke_data.type_ids[move_data["type"]], round(move_data["accuracy"] * 2.55), move_data["pp"]]), address)
|
||||
|
||||
TM_IDs = [poke_data.moves[move]["id"] for move in world.local_tms]
|
||||
write_bytes(rom_addresses["TM_Moves"], TM_IDs)
|
||||
TM_IDs = bytearray([poke_data.moves[move]["id"] for move in world.local_tms])
|
||||
write_bytes(data, TM_IDs, rom_addresses["TM_Moves"])
|
||||
|
||||
if world.options.randomize_rock_tunnel:
|
||||
seed = randomize_rock_tunnel(patch, world.random)
|
||||
write_bytes(rom_addresses["Text_Rock_Tunnel_Sign"], encode_text(f"SEED: <LINE>{seed}"))
|
||||
seed = randomize_rock_tunnel(data, random)
|
||||
write_bytes(data, encode_text(f"SEED: <LINE>{seed}"), rom_addresses["Text_Rock_Tunnel_Sign"])
|
||||
|
||||
mons = [mon["id"] for mon in poke_data.pokemon_data.values()]
|
||||
world.random.shuffle(mons)
|
||||
write_bytes(rom_addresses["Title_Mon_First"], mons.pop())
|
||||
random.shuffle(mons)
|
||||
data[rom_addresses['Title_Mon_First']] = mons.pop()
|
||||
for mon in range(0, 16):
|
||||
write_bytes(rom_addresses["Title_Mons"] + mon, mons.pop())
|
||||
data[rom_addresses['Title_Mons'] + mon] = mons.pop()
|
||||
if world.options.game_version.value:
|
||||
mons.sort(key=lambda mon: 0 if mon == world.multiworld.get_location("Oak's Lab - Starter 1", world.player).item.name
|
||||
else 1 if mon == world.multiworld.get_location("Oak's Lab - Starter 2", world.player).item.name else
|
||||
@@ -622,34 +576,34 @@ def generate_output(world: "PokemonRedBlueWorld", output_directory: str):
|
||||
mons.sort(key=lambda mon: 0 if mon == world.multiworld.get_location("Oak's Lab - Starter 2", world.player).item.name
|
||||
else 1 if mon == world.multiworld.get_location("Oak's Lab - Starter 1", world.player).item.name else
|
||||
2 if mon == world.multiworld.get_location("Oak's Lab - Starter 3", world.player).item.name else 3)
|
||||
write_bytes(rom_addresses["Title_Seed"], encode_text(world.multiworld.seed_name[-20:], 20, True))
|
||||
write_bytes(data, encode_text(world.multiworld.seed_name[-20:], 20, True), rom_addresses['Title_Seed'])
|
||||
|
||||
slot_name = world.multiworld.player_name[world.player]
|
||||
slot_name.replace("@", " ")
|
||||
slot_name.replace("<", " ")
|
||||
slot_name.replace(">", " ")
|
||||
write_bytes(rom_addresses["Title_Slot_Name"], encode_text(slot_name, 16, True, True))
|
||||
write_bytes(data, encode_text(slot_name, 16, True, True), rom_addresses['Title_Slot_Name'])
|
||||
|
||||
if world.trainer_name == "choose_in_game":
|
||||
write_bytes(rom_addresses["Skip_Player_Name"], 0)
|
||||
data[rom_addresses["Skip_Player_Name"]] = 0
|
||||
else:
|
||||
write_bytes(rom_addresses["Player_Name"], world.trainer_name)
|
||||
write_bytes(data, world.trainer_name, rom_addresses['Player_Name'])
|
||||
if world.rival_name == "choose_in_game":
|
||||
write_bytes(rom_addresses["Skip_Rival_Name"], 0)
|
||||
data[rom_addresses["Skip_Rival_Name"]] = 0
|
||||
else:
|
||||
write_bytes(rom_addresses["Rival_Name"], world.rival_name)
|
||||
write_bytes(data, world.rival_name, rom_addresses['Rival_Name'])
|
||||
|
||||
write_bytes(0xFF00, 2) # client compatibility version
|
||||
rom_name = bytearray(f"AP{Utils.__version__.replace('.', '')[0:3]}_{world.player}_{world.multiworld.seed:11}\0",
|
||||
"utf8")[:21]
|
||||
data[0xFF00] = 2 # client compatibility version
|
||||
rom_name = bytearray(f'AP{Utils.__version__.replace(".", "")[0:3]}_{world.player}_{world.multiworld.seed:11}\0',
|
||||
'utf8')[:21]
|
||||
rom_name.extend([0] * (21 - len(rom_name)))
|
||||
write_bytes(0xFFC6, rom_name)
|
||||
write_bytes(0xFFDB, world.multiworld.seed_name.encode())
|
||||
write_bytes(0xFFF0, world.multiworld.player_name[world.player].encode())
|
||||
write_bytes(data, rom_name, 0xFFC6)
|
||||
write_bytes(data, world.multiworld.seed_name.encode(), 0xFFDB)
|
||||
write_bytes(data, world.multiworld.player_name[world.player].encode(), 0xFFF0)
|
||||
|
||||
world.finished_level_scaling.wait()
|
||||
|
||||
write_quizzes(world, patch)
|
||||
write_quizzes(world, data, random)
|
||||
|
||||
for location in world.multiworld.get_locations(world.player):
|
||||
if location.party_data:
|
||||
@@ -663,18 +617,18 @@ def generate_output(world: "PokemonRedBlueWorld", output_directory: str):
|
||||
levels = party["level"]
|
||||
for address, party in zip(addresses, parties):
|
||||
if isinstance(levels, int):
|
||||
write_bytes(address, levels)
|
||||
data[address] = levels
|
||||
address += 1
|
||||
for mon in party:
|
||||
write_bytes(address, poke_data.pokemon_data[mon]["id"])
|
||||
data[address] = poke_data.pokemon_data[mon]["id"]
|
||||
address += 1
|
||||
else:
|
||||
address += 1
|
||||
for level, mon in zip(levels, party):
|
||||
write_bytes(address, [level, poke_data.pokemon_data[mon]["id"]])
|
||||
data[address] = level
|
||||
data[address + 1] = poke_data.pokemon_data[mon]["id"]
|
||||
address += 2
|
||||
# This assert can't be done with procedure patch tokens.
|
||||
# assert data[address] == 0 or location.name == "Fossil Level - Trainer Parties"
|
||||
assert data[address] == 0 or location.name == "Fossil Level - Trainer Parties"
|
||||
continue
|
||||
elif location.rom_address is None:
|
||||
continue
|
||||
@@ -685,24 +639,85 @@ def generate_output(world: "PokemonRedBlueWorld", output_directory: str):
|
||||
rom_address = [rom_address]
|
||||
for address in rom_address:
|
||||
if location.item.name in poke_data.pokemon_data.keys():
|
||||
write_bytes(address, poke_data.pokemon_data[location.item.name]["id"])
|
||||
data[address] = poke_data.pokemon_data[location.item.name]["id"]
|
||||
elif " ".join(location.item.name.split()[1:]) in poke_data.pokemon_data.keys():
|
||||
write_bytes(address, poke_data.pokemon_data[" ".join(location.item.name.split()[1:])]["id"])
|
||||
data[address] = poke_data.pokemon_data[" ".join(location.item.name.split()[1:])]["id"]
|
||||
else:
|
||||
item_id = world.item_name_to_id[location.item.name] - 172000000
|
||||
if item_id > 255:
|
||||
item_id -= 256
|
||||
write_bytes(address, item_id)
|
||||
data[address] = item_id
|
||||
if location.level:
|
||||
write_bytes(location.level_address, location.level)
|
||||
data[location.level_address] = location.level
|
||||
|
||||
else:
|
||||
rom_address = location.rom_address
|
||||
if not isinstance(rom_address, list):
|
||||
rom_address = [rom_address]
|
||||
for address in rom_address:
|
||||
write_bytes(address, 0x2C) # AP Item
|
||||
data[address] = 0x2C # AP Item
|
||||
|
||||
patch.write_file("token_data.bin", patch.get_token_binary())
|
||||
out_file_name = world.multiworld.get_out_file_name_base(world.player)
|
||||
patch.write(os.path.join(output_directory, f"{out_file_name}{patch.patch_file_ending}"))
|
||||
outfilepname = f'_P{world.player}'
|
||||
outfilepname += f"_{world.multiworld.get_file_safe_player_name(world.player).replace(' ', '_')}" \
|
||||
if world.multiworld.player_name[world.player] != 'Player%d' % world.player else ''
|
||||
rompath = os.path.join(output_directory, f'AP_{world.multiworld.seed_name}{outfilepname}.gb')
|
||||
with open(rompath, 'wb') as outfile:
|
||||
outfile.write(data)
|
||||
if world.options.game_version.current_key == "red":
|
||||
patch = RedDeltaPatch(os.path.splitext(rompath)[0] + RedDeltaPatch.patch_file_ending, player=world.player,
|
||||
player_name=world.multiworld.player_name[world.player], patched_path=rompath)
|
||||
else:
|
||||
patch = BlueDeltaPatch(os.path.splitext(rompath)[0] + BlueDeltaPatch.patch_file_ending, player=world.player,
|
||||
player_name=world.multiworld.player_name[world.player], patched_path=rompath)
|
||||
|
||||
patch.write()
|
||||
os.unlink(rompath)
|
||||
|
||||
|
||||
def write_bytes(data, byte_array, address):
|
||||
for byte in byte_array:
|
||||
data[address] = byte
|
||||
address += 1
|
||||
|
||||
|
||||
def get_base_rom_bytes(game_version: str, hash: str="") -> bytes:
|
||||
file_name = get_base_rom_path(game_version)
|
||||
with open(file_name, "rb") as file:
|
||||
base_rom_bytes = bytes(file.read())
|
||||
if hash:
|
||||
basemd5 = hashlib.md5()
|
||||
basemd5.update(base_rom_bytes)
|
||||
if hash != basemd5.hexdigest():
|
||||
raise Exception(f"Supplied Base Rom does not match known MD5 for Pokémon {game_version.title()} UE "
|
||||
"release. Get the correct game and version, then dump it")
|
||||
return base_rom_bytes
|
||||
|
||||
|
||||
def get_base_rom_path(game_version: str) -> str:
|
||||
options = Utils.get_options()
|
||||
file_name = options["pokemon_rb_options"][f"{game_version}_rom_file"]
|
||||
if not os.path.exists(file_name):
|
||||
file_name = Utils.user_path(file_name)
|
||||
return file_name
|
||||
|
||||
|
||||
class BlueDeltaPatch(APDeltaPatch):
|
||||
patch_file_ending = ".apblue"
|
||||
hash = "50927e843568814f7ed45ec4f944bd8b"
|
||||
game_version = "blue"
|
||||
game = "Pokemon Red and Blue"
|
||||
result_file_ending = ".gb"
|
||||
@classmethod
|
||||
def get_source_data(cls) -> bytes:
|
||||
return get_base_rom_bytes(cls.game_version, cls.hash)
|
||||
|
||||
|
||||
class RedDeltaPatch(APDeltaPatch):
|
||||
patch_file_ending = ".apred"
|
||||
hash = "3d45c1ee9abd5738df46d2bdda8b57dc"
|
||||
game_version = "red"
|
||||
game = "Pokemon Red and Blue"
|
||||
result_file_ending = ".gb"
|
||||
@classmethod
|
||||
def get_source_data(cls) -> bytes:
|
||||
return get_base_rom_bytes(cls.game_version, cls.hash)
|
||||
|
||||
@@ -66,8 +66,11 @@ def get_plando_locations(world: World) -> List[str]:
|
||||
if world is None:
|
||||
return []
|
||||
plando_locations = []
|
||||
for plando_setting in world.options.plando_items:
|
||||
plando_locations += plando_setting.locations
|
||||
for plando_setting in world.multiworld.plando_items[world.player]:
|
||||
plando_locations += plando_setting.get("locations", [])
|
||||
plando_setting_location = plando_setting.get("location", None)
|
||||
if plando_setting_location is not None:
|
||||
plando_locations.append(plando_setting_location)
|
||||
|
||||
return plando_locations
|
||||
|
||||
|
||||
@@ -245,7 +245,7 @@ class ShiversWorld(World):
|
||||
|
||||
storage_items += [self.create_item("Empty") for _ in range(3)]
|
||||
|
||||
state = self.multiworld.get_all_state(False, True, False)
|
||||
state = self.multiworld.get_all_state(False)
|
||||
|
||||
self.random.shuffle(storage_locs)
|
||||
self.random.shuffle(storage_items)
|
||||
@@ -255,27 +255,6 @@ class ShiversWorld(World):
|
||||
self.storage_placements = {location.name.replace("Storage: ", ""): location.item.name.replace(" DUPE", "") for
|
||||
location in storage_locs}
|
||||
|
||||
def get_pre_fill_items(self) -> List[Item]:
|
||||
if self.options.full_pots == "pieces":
|
||||
return [self.create_item(name) for name, data in item_table.items() if
|
||||
data.type == ItemType.POT_DUPLICATE]
|
||||
elif self.options.full_pots == "complete":
|
||||
return [self.create_item(name) for name, data in item_table.items() if
|
||||
data.type == ItemType.POT_COMPELTE_DUPLICATE]
|
||||
else:
|
||||
pool = []
|
||||
pieces = [self.create_item(name) for name, data in item_table.items() if
|
||||
data.type == ItemType.POT_DUPLICATE]
|
||||
complete = [self.create_item(name) for name, data in item_table.items() if
|
||||
data.type == ItemType.POT_COMPELTE_DUPLICATE]
|
||||
for i in range(10):
|
||||
if self.pot_completed_list[i] == 0:
|
||||
pool.append(pieces[i])
|
||||
pool.append(pieces[i + 10])
|
||||
else:
|
||||
pool.append(complete[i])
|
||||
return pool
|
||||
|
||||
def fill_slot_data(self) -> dict:
|
||||
return {
|
||||
"StoragePlacements": self.storage_placements,
|
||||
|
||||
@@ -89,12 +89,9 @@ class ROM(object):
|
||||
|
||||
class FakeROM(ROM):
|
||||
# to have the same code for real ROM and the webservice
|
||||
def __init__(self, data=None):
|
||||
def __init__(self, data={}):
|
||||
super(FakeROM, self).__init__()
|
||||
if data is None:
|
||||
self.data = {}
|
||||
else:
|
||||
self.data = data
|
||||
self.data = data
|
||||
self.ipsPatches = []
|
||||
|
||||
def write(self, bytes):
|
||||
|
||||
@@ -1,15 +1,6 @@
|
||||
# Super Mario World - Changelog
|
||||
|
||||
|
||||
## v2.1
|
||||
|
||||
### Features:
|
||||
- Trap Link
|
||||
- When you receive a trap, you send a copy of it to every other player with Trap Link enabled
|
||||
- Ring Link
|
||||
- Any coin amounts gained and lost by a linked player will be instantly shared with all other active linked players
|
||||
|
||||
|
||||
## v2.0
|
||||
|
||||
### Features:
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import logging
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
from NetUtils import ClientStatus, NetworkItem, color
|
||||
from NetUtils import ClientStatus, color
|
||||
from worlds.AutoSNIClient import SNIClient
|
||||
from .Names.TextBox import generate_received_text, generate_received_trap_link_text
|
||||
from .Items import trap_value_to_name, trap_name_to_value
|
||||
from .Names.TextBox import generate_received_text
|
||||
|
||||
snes_logger = logging.getLogger("SNES")
|
||||
|
||||
@@ -44,13 +42,10 @@ SMW_MOON_ACTIVE_ADDR = ROM_START + 0x01BFA8
|
||||
SMW_HIDDEN_1UP_ACTIVE_ADDR = ROM_START + 0x01BFA9
|
||||
SMW_BONUS_BLOCK_ACTIVE_ADDR = ROM_START + 0x01BFAA
|
||||
SMW_BLOCKSANITY_ACTIVE_ADDR = ROM_START + 0x01BFAB
|
||||
SMW_TRAP_LINK_ACTIVE_ADDR = ROM_START + 0x01BFB7
|
||||
SMW_RING_LINK_ACTIVE_ADDR = ROM_START + 0x01BFB8
|
||||
|
||||
|
||||
SMW_GAME_STATE_ADDR = WRAM_START + 0x100
|
||||
SMW_MARIO_STATE_ADDR = WRAM_START + 0x71
|
||||
SMW_COIN_COUNT_ADDR = WRAM_START + 0xDBF
|
||||
SMW_BOSS_STATE_ADDR = WRAM_START + 0xD9B
|
||||
SMW_ACTIVE_BOSS_ADDR = WRAM_START + 0x13FC
|
||||
SMW_CURRENT_LEVEL_ADDR = WRAM_START + 0x13BF
|
||||
@@ -81,7 +76,6 @@ SMW_UNCOLLECTABLE_DRAGON_COINS = [0x24]
|
||||
class SMWSNIClient(SNIClient):
|
||||
game = "Super Mario World"
|
||||
patch_suffix = ".apsmw"
|
||||
slot_data: dict[str, Any] | None
|
||||
|
||||
async def deathlink_kill_player(self, ctx):
|
||||
from SNIClient import DeathState, snes_buffered_write, snes_flush_writes, snes_read
|
||||
@@ -117,84 +111,6 @@ class SMWSNIClient(SNIClient):
|
||||
ctx.last_death_link = time.time()
|
||||
|
||||
|
||||
def on_package(self, ctx: SNIClient, cmd: str, args: dict[str, Any]) -> None:
|
||||
super().on_package(ctx, cmd, args)
|
||||
|
||||
if cmd == "Connected":
|
||||
self.slot_data = args.get("slot_data", None)
|
||||
|
||||
if cmd != "Bounced":
|
||||
return
|
||||
if "tags" not in args:
|
||||
return
|
||||
|
||||
if not hasattr(self, "instance_id"):
|
||||
self.instance_id = time.time()
|
||||
|
||||
source_name = args["data"]["source"]
|
||||
if "TrapLink" in ctx.tags and "TrapLink" in args["tags"] and source_name != ctx.slot_info[ctx.slot].name:
|
||||
trap_name: str = args["data"]["trap_name"]
|
||||
if trap_name not in trap_name_to_value:
|
||||
# We don't know how to handle this trap, ignore it
|
||||
return
|
||||
|
||||
trap_id: int = trap_name_to_value[trap_name]
|
||||
|
||||
if "trap_weights" not in self.slot_data:
|
||||
return
|
||||
|
||||
if f"{trap_id}" not in self.slot_data["trap_weights"]:
|
||||
return
|
||||
|
||||
if self.slot_data["trap_weights"][f"{trap_id}"] == 0:
|
||||
# The player disabled this trap type
|
||||
return
|
||||
|
||||
self.priority_trap = NetworkItem(trap_id, None, None)
|
||||
self.priority_trap_message = generate_received_trap_link_text(trap_name, source_name)
|
||||
self.priority_trap_message_str = f"Received linked {trap_name} from {source_name}"
|
||||
elif "RingLink" in ctx.tags and "RingLink" in args["tags"] and source_name != self.instance_id:
|
||||
if not hasattr(self, "pending_ring_link"):
|
||||
self.pending_ring_link = 0
|
||||
self.pending_ring_link += args["data"]["amount"]
|
||||
|
||||
async def send_trap_link(self, ctx: SNIClient, trap_name: str):
|
||||
if "TrapLink" not in ctx.tags or ctx.slot == None:
|
||||
return
|
||||
|
||||
await ctx.send_msgs([{
|
||||
"cmd": "Bounce", "tags": ["TrapLink"],
|
||||
"data": {
|
||||
"time": time.time(),
|
||||
"source": ctx.player_names[ctx.slot],
|
||||
"trap_name": trap_name
|
||||
}
|
||||
}])
|
||||
snes_logger.info(f"Sent linked {trap_name}")
|
||||
|
||||
async def send_ring_link(self, ctx: SNIClient, amount: int):
|
||||
from SNIClient import DeathState, snes_buffered_write, snes_flush_writes, snes_read
|
||||
|
||||
if "RingLink" not in ctx.tags or ctx.slot == None:
|
||||
return
|
||||
|
||||
game_state = await snes_read(ctx, SMW_GAME_STATE_ADDR, 0x1)
|
||||
if game_state[0] != 0x14:
|
||||
return
|
||||
|
||||
if not hasattr(self, "instance_id"):
|
||||
self.instance_id = time.time()
|
||||
|
||||
await ctx.send_msgs([{
|
||||
"cmd": "Bounce", "tags": ["RingLink"],
|
||||
"data": {
|
||||
"time": time.time(),
|
||||
"source": self.instance_id,
|
||||
"amount": amount
|
||||
}
|
||||
}])
|
||||
|
||||
|
||||
async def validate_rom(self, ctx):
|
||||
from SNIClient import snes_buffered_write, snes_flush_writes, snes_read
|
||||
|
||||
@@ -207,11 +123,9 @@ class SMWSNIClient(SNIClient):
|
||||
|
||||
receive_option = await snes_read(ctx, SMW_RECEIVE_MSG_DATA, 0x1)
|
||||
send_option = await snes_read(ctx, SMW_SEND_MSG_DATA, 0x1)
|
||||
trap_link = await snes_read(ctx, SMW_TRAP_LINK_ACTIVE_ADDR, 0x1)
|
||||
|
||||
ctx.receive_option = receive_option[0]
|
||||
ctx.send_option = send_option[0]
|
||||
ctx.trap_link = trap_link[0]
|
||||
|
||||
ctx.allow_collect = True
|
||||
|
||||
@@ -219,15 +133,6 @@ class SMWSNIClient(SNIClient):
|
||||
if death_link:
|
||||
await ctx.update_death_link(bool(death_link[0] & 0b1))
|
||||
|
||||
if trap_link and bool(trap_link[0] & 0b1) and "TrapLink" not in ctx.tags:
|
||||
ctx.tags.add("TrapLink")
|
||||
await ctx.send_msgs([{"cmd": "ConnectUpdate", "tags": ctx.tags}])
|
||||
|
||||
ring_link = await snes_read(ctx, SMW_RING_LINK_ACTIVE_ADDR, 1)
|
||||
if ring_link and bool(ring_link[0] & 0b1) and "RingLink" not in ctx.tags:
|
||||
ctx.tags.add("RingLink")
|
||||
await ctx.send_msgs([{"cmd": "ConnectUpdate", "tags": ctx.tags}])
|
||||
|
||||
if ctx.rom != rom_name:
|
||||
ctx.current_sublevel_value = 0
|
||||
|
||||
@@ -237,17 +142,12 @@ class SMWSNIClient(SNIClient):
|
||||
|
||||
|
||||
def add_message_to_queue(self, new_message):
|
||||
|
||||
if not hasattr(self, "message_queue"):
|
||||
self.message_queue = []
|
||||
|
||||
self.message_queue.append(new_message)
|
||||
|
||||
def add_message_to_queue_front(self, new_message):
|
||||
if not hasattr(self, "message_queue"):
|
||||
self.message_queue = []
|
||||
|
||||
self.message_queue.insert(0, new_message)
|
||||
|
||||
|
||||
async def handle_message_queue(self, ctx):
|
||||
from SNIClient import snes_buffered_write, snes_flush_writes, snes_read
|
||||
@@ -306,8 +206,7 @@ class SMWSNIClient(SNIClient):
|
||||
async def handle_trap_queue(self, ctx):
|
||||
from SNIClient import snes_buffered_write, snes_flush_writes, snes_read
|
||||
|
||||
if (not hasattr(self, "trap_queue") or len(self.trap_queue) == 0) and\
|
||||
(not hasattr(self, "priority_trap") or self.priority_trap == 0):
|
||||
if not hasattr(self, "trap_queue") or len(self.trap_queue) == 0:
|
||||
return
|
||||
|
||||
game_state = await snes_read(ctx, SMW_GAME_STATE_ADDR, 0x1)
|
||||
@@ -322,24 +221,7 @@ class SMWSNIClient(SNIClient):
|
||||
if pause_state[0] != 0x00:
|
||||
return
|
||||
|
||||
|
||||
next_trap = None
|
||||
message = bytearray()
|
||||
message_str = ""
|
||||
from_queue = False
|
||||
|
||||
if getattr(self, "priority_trap", None) and self.priority_trap.item != 0:
|
||||
next_trap = self.priority_trap
|
||||
message = self.priority_trap_message
|
||||
message_str = self.priority_trap_message_str
|
||||
self.priority_trap = None
|
||||
self.priority_trap_message = bytearray()
|
||||
self.priority_trap_message_str = ""
|
||||
elif hasattr(self, "trap_queue") and len(self.trap_queue) > 0:
|
||||
from_queue = True
|
||||
next_trap, message = self.trap_queue.pop(0)
|
||||
else:
|
||||
return
|
||||
next_trap, message = self.trap_queue.pop(0)
|
||||
|
||||
from .Rom import trap_rom_data
|
||||
if next_trap.item in trap_rom_data:
|
||||
@@ -349,22 +231,16 @@ class SMWSNIClient(SNIClient):
|
||||
# Timer Trap
|
||||
if trap_active[0] == 0 or (trap_active[0] == 1 and trap_active[1] == 0 and trap_active[2] == 0):
|
||||
# Trap already active
|
||||
if from_queue:
|
||||
self.add_trap_to_queue(next_trap, message)
|
||||
self.add_trap_to_queue(next_trap, message)
|
||||
return
|
||||
else:
|
||||
if len(message_str) > 0:
|
||||
snes_logger.info(message_str)
|
||||
if "TrapLink" in ctx.tags and from_queue:
|
||||
await self.send_trap_link(ctx, trap_value_to_name[next_trap.item])
|
||||
snes_buffered_write(ctx, WRAM_START + trap_rom_data[next_trap.item][0], bytes([0x01]))
|
||||
snes_buffered_write(ctx, WRAM_START + trap_rom_data[next_trap.item][0] + 1, bytes([0x00]))
|
||||
snes_buffered_write(ctx, WRAM_START + trap_rom_data[next_trap.item][0] + 2, bytes([0x00]))
|
||||
else:
|
||||
if trap_active[0] > 0:
|
||||
# Trap already active
|
||||
if from_queue:
|
||||
self.add_trap_to_queue(next_trap, message)
|
||||
self.add_trap_to_queue(next_trap, message)
|
||||
return
|
||||
else:
|
||||
if next_trap.item == 0xBC001D:
|
||||
@@ -372,18 +248,12 @@ class SMWSNIClient(SNIClient):
|
||||
# Do not fire if the previous thwimp hasn't reached the player's Y pos
|
||||
active_thwimp = await snes_read(ctx, SMW_ACTIVE_THWIMP_ADDR, 0x1)
|
||||
if active_thwimp[0] != 0xFF:
|
||||
if from_queue:
|
||||
self.add_trap_to_queue(next_trap, message)
|
||||
self.add_trap_to_queue(next_trap, message)
|
||||
return
|
||||
verify_game_state = await snes_read(ctx, SMW_GAME_STATE_ADDR, 0x1)
|
||||
if verify_game_state[0] == 0x14 and len(trap_rom_data[next_trap.item]) > 2:
|
||||
snes_buffered_write(ctx, SMW_SFX_ADDR, bytes([trap_rom_data[next_trap.item][2]]))
|
||||
|
||||
if len(message_str) > 0:
|
||||
snes_logger.info(message_str)
|
||||
if "TrapLink" in ctx.tags and from_queue:
|
||||
await self.send_trap_link(ctx, trap_value_to_name[next_trap.item])
|
||||
|
||||
new_item_count = trap_rom_data[next_trap.item][1]
|
||||
snes_buffered_write(ctx, WRAM_START + trap_rom_data[next_trap.item][0], bytes([new_item_count]))
|
||||
|
||||
@@ -400,75 +270,9 @@ class SMWSNIClient(SNIClient):
|
||||
return
|
||||
|
||||
if self.should_show_message(ctx, next_trap):
|
||||
self.add_message_to_queue_front(message)
|
||||
elif next_trap.item == 0xBC0015:
|
||||
if self.should_show_message(ctx, next_trap):
|
||||
self.add_message_to_queue_front(message)
|
||||
if len(message_str) > 0:
|
||||
snes_logger.info(message_str)
|
||||
if "TrapLink" in ctx.tags and from_queue:
|
||||
await self.send_trap_link(ctx, trap_value_to_name[next_trap.item])
|
||||
|
||||
# Handle Literature Trap
|
||||
from .Names.LiteratureTrap import lit_trap_text_list
|
||||
import random
|
||||
rand_trap = random.choice(lit_trap_text_list)
|
||||
|
||||
for message in rand_trap:
|
||||
self.add_message_to_queue(message)
|
||||
|
||||
|
||||
async def handle_ring_link(self, ctx):
|
||||
from SNIClient import snes_buffered_write, snes_flush_writes, snes_read
|
||||
|
||||
if "RingLink" not in ctx.tags:
|
||||
return
|
||||
|
||||
if not hasattr(self, "prev_coins"):
|
||||
self.prev_coins = 0
|
||||
|
||||
curr_coins_byte = await snes_read(ctx, SMW_COIN_COUNT_ADDR, 0x1)
|
||||
curr_coins = curr_coins_byte[0]
|
||||
|
||||
if curr_coins < self.prev_coins:
|
||||
# Coins rolled over from 1-Up
|
||||
curr_coins += 100
|
||||
|
||||
coins_diff = curr_coins - self.prev_coins
|
||||
if coins_diff > 0:
|
||||
await self.send_ring_link(ctx, coins_diff)
|
||||
self.prev_coins = curr_coins % 100
|
||||
|
||||
new_coins = curr_coins
|
||||
if not hasattr(self, "pending_ring_link"):
|
||||
self.pending_ring_link = 0
|
||||
|
||||
if self.pending_ring_link != 0:
|
||||
new_coins += self.pending_ring_link
|
||||
new_coins = max(new_coins, 0)
|
||||
|
||||
new_1_ups = 0
|
||||
while new_coins >= 100:
|
||||
new_1_ups += 1
|
||||
new_coins -= 100
|
||||
|
||||
if new_1_ups > 0:
|
||||
curr_lives_inc_byte = await snes_read(ctx, WRAM_START + 0x18E4, 0x1)
|
||||
curr_lives_inc = curr_lives_inc_byte[0]
|
||||
new_lives_inc = curr_lives_inc + new_1_ups
|
||||
snes_buffered_write(ctx, WRAM_START + 0x18E4, bytes([new_lives_inc]))
|
||||
|
||||
snes_buffered_write(ctx, SMW_COIN_COUNT_ADDR, bytes([new_coins]))
|
||||
if self.pending_ring_link > 0:
|
||||
snes_buffered_write(ctx, SMW_SFX_ADDR, bytes([0x01]))
|
||||
else:
|
||||
snes_buffered_write(ctx, SMW_SFX_ADDR, bytes([0x2A]))
|
||||
self.pending_ring_link = 0
|
||||
self.prev_coins = new_coins
|
||||
|
||||
await snes_flush_writes(ctx)
|
||||
|
||||
|
||||
async def game_watcher(self, ctx):
|
||||
from SNIClient import snes_buffered_write, snes_flush_writes, snes_read
|
||||
|
||||
@@ -529,7 +333,6 @@ class SMWSNIClient(SNIClient):
|
||||
|
||||
await self.handle_message_queue(ctx)
|
||||
await self.handle_trap_queue(ctx)
|
||||
await self.handle_ring_link(ctx)
|
||||
|
||||
new_checks = []
|
||||
event_data = await snes_read(ctx, SMW_EVENT_ROM_DATA, 0x60)
|
||||
@@ -703,7 +506,7 @@ class SMWSNIClient(SNIClient):
|
||||
ctx.location_names.lookup_in_slot(item.location, item.player), recv_index, len(ctx.items_received)))
|
||||
|
||||
if self.should_show_message(ctx, item):
|
||||
if item.item != 0xBC0012 and item.item != 0xBC0015 and item.item not in trap_rom_data:
|
||||
if item.item != 0xBC0012 and item.item not in trap_rom_data:
|
||||
# Don't send messages for Boss Tokens
|
||||
item_name = ctx.item_names.lookup_in_game(item.item)
|
||||
player_name = ctx.player_names[item.player]
|
||||
@@ -712,7 +515,7 @@ class SMWSNIClient(SNIClient):
|
||||
self.add_message_to_queue(receive_message)
|
||||
|
||||
snes_buffered_write(ctx, SMW_RECV_PROGRESS_ADDR, bytes([recv_index&0xFF, (recv_index>>8)&0xFF]))
|
||||
if item.item in trap_rom_data or item.item == 0xBC0015:
|
||||
if item.item in trap_rom_data:
|
||||
item_name = ctx.item_names.lookup_in_game(item.item)
|
||||
player_name = ctx.player_names[item.player]
|
||||
|
||||
@@ -769,6 +572,14 @@ class SMWSNIClient(SNIClient):
|
||||
else:
|
||||
# Extra Powerup?
|
||||
pass
|
||||
elif item.item == 0xBC0015:
|
||||
# Handle Literature Trap
|
||||
from .Names.LiteratureTrap import lit_trap_text_list
|
||||
import random
|
||||
rand_trap = random.choice(lit_trap_text_list)
|
||||
|
||||
for message in rand_trap:
|
||||
self.add_message_to_queue(message)
|
||||
|
||||
await snes_flush_writes(ctx)
|
||||
|
||||
|
||||
@@ -75,49 +75,3 @@ item_table = {
|
||||
}
|
||||
|
||||
lookup_id_to_name: typing.Dict[int, str] = {data.code: item_name for item_name, data in item_table.items() if data.code}
|
||||
|
||||
|
||||
trap_value_to_name: typing.Dict[int, str] = {
|
||||
0xBC0013: ItemName.ice_trap,
|
||||
0xBC0014: ItemName.stun_trap,
|
||||
0xBC0015: ItemName.literature_trap,
|
||||
0xBC0016: ItemName.timer_trap,
|
||||
0xBC001C: ItemName.reverse_controls_trap,
|
||||
0xBC001D: ItemName.thwimp_trap,
|
||||
}
|
||||
|
||||
trap_name_to_value: typing.Dict[str, int] = {
|
||||
# Our native Traps
|
||||
ItemName.ice_trap: 0xBC0013,
|
||||
ItemName.stun_trap: 0xBC0014,
|
||||
ItemName.literature_trap: 0xBC0015,
|
||||
ItemName.timer_trap: 0xBC0016,
|
||||
ItemName.reverse_controls_trap: 0xBC001C,
|
||||
ItemName.thwimp_trap: 0xBC001D,
|
||||
|
||||
# Common other trap names
|
||||
"Chaos Control Trap": 0xBC0014, # Stun Trap
|
||||
"Confuse Trap": 0xBC001C, # Reverse Trap
|
||||
"Exposition Trap": 0xBC0015, # Literature Trap
|
||||
"Cutscene Trap": 0xBC0015, # Literature Trap
|
||||
"Freeze Trap": 0xBC0014, # Stun Trap
|
||||
"Frozen Trap": 0xBC0014, # Stun Trap
|
||||
"Paralyze Trap": 0xBC0014, # Stun Trap
|
||||
"Reversal Trap": 0xBC001C, # Reverse Trap
|
||||
"Fuzzy Trap": 0xBC001C, # Reverse Trap
|
||||
"Confound Trap": 0xBC001C, # Reverse Trap
|
||||
"Confusion Trap": 0xBC001C, # Reverse Trap
|
||||
"Police Trap": 0xBC001D, # Thwimp Trap
|
||||
"Buyon Trap": 0xBC001D, # Thwimp Trap
|
||||
"Gooey Bag": 0xBC001D, # Thwimp Trap
|
||||
"TNT Barrel Trap": 0xBC001D, # Thwimp Trap
|
||||
"Honey Trap": 0xBC0014, # Stun Trap
|
||||
"Screen Flip Trap": 0xBC001C, # Reverse Trap
|
||||
"Banana Trap": 0xBC0013, # Ice Trap
|
||||
"Bomb": 0xBC001D, # Thwimp Trap
|
||||
"Bonk Trap": 0xBC0014, # Stun Trap
|
||||
"Ghost": 0xBC001D, # Thwimp Trap
|
||||
"Fast Trap": 0xBC0016, # Timer Trap
|
||||
"Nut Trap": 0xBC001D, # Thwimp Trap
|
||||
"Army Trap": 0xBC001D, # Thwimp Trap
|
||||
}
|
||||
|
||||
@@ -117,31 +117,6 @@ def generate_received_text(item_name: str, player_name: str):
|
||||
return out_array
|
||||
|
||||
|
||||
def generate_received_trap_link_text(item_name: str, player_name: str):
|
||||
out_array = bytearray()
|
||||
|
||||
item_name = item_name[:18]
|
||||
player_name = player_name[:18]
|
||||
|
||||
item_buffer = max(0, math.floor((18 - len(item_name)) / 2))
|
||||
player_buffer = max(0, math.floor((18 - len(player_name)) / 2))
|
||||
|
||||
out_array += bytearray([0x9F, 0x9F])
|
||||
out_array += string_to_bytes(" Received linked")
|
||||
out_array[-1] += 0x80
|
||||
out_array += bytearray([0x1F] * item_buffer)
|
||||
out_array += string_to_bytes(item_name)
|
||||
out_array[-1] += 0x80
|
||||
out_array += string_to_bytes(" from")
|
||||
out_array[-1] += 0x80
|
||||
out_array += bytearray([0x1F] * player_buffer)
|
||||
out_array += string_to_bytes(player_name)
|
||||
out_array[-1] += 0x80
|
||||
out_array += bytearray([0x9F, 0x9F])
|
||||
|
||||
return out_array
|
||||
|
||||
|
||||
def generate_sent_text(item_name: str, player_name: str):
|
||||
out_array = bytearray()
|
||||
|
||||
|
||||
@@ -398,20 +398,6 @@ class StartingLifeCount(Range):
|
||||
default = 5
|
||||
|
||||
|
||||
class RingLink(Toggle):
|
||||
"""
|
||||
Whether your in-level coin gain/loss is linked to other players
|
||||
"""
|
||||
display_name = "Ring Link"
|
||||
|
||||
|
||||
class TrapLink(Toggle):
|
||||
"""
|
||||
Whether your received traps are linked to other players
|
||||
"""
|
||||
display_name = "Trap Link"
|
||||
|
||||
|
||||
smw_option_groups = [
|
||||
OptionGroup("Goal Options", [
|
||||
Goal,
|
||||
@@ -461,8 +447,6 @@ smw_option_groups = [
|
||||
@dataclass
|
||||
class SMWOptions(PerGameCommonOptions):
|
||||
death_link: DeathLink
|
||||
ring_link: RingLink
|
||||
trap_link: TrapLink
|
||||
goal: Goal
|
||||
bosses_required: BossesRequired
|
||||
max_yoshi_egg_cap: NumberOfYoshiEggs
|
||||
|
||||
@@ -719,8 +719,8 @@ def handle_vertical_scroll(rom):
|
||||
0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x01, 0x02, # Levels 0D0-0DF
|
||||
0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x01, 0x02, 0x02, 0x02, 0x01, 0x02, 0x02, # Levels 0E0-0EF
|
||||
0x02, 0x02, 0x01, 0x02, 0x02, 0x01, 0x01, 0x02, 0x02, 0x01, 0x02, 0x02, 0x02, 0x02, 0x01, 0x02, # Levels 0F0-0FF
|
||||
0x02, 0x02, 0x02, 0x02, 0x02, 0x01, 0x01, 0x01, 0x02, 0x02, 0x02, 0x01, 0x02, 0x02, 0x02, 0x01, # Levels 100-10F
|
||||
0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x01, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x01, # Levels 110-11F
|
||||
0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x01, 0x01, 0x02, 0x02, 0x02, 0x01, 0x02, 0x02, 0x02, 0x01, # Levels 100-10F
|
||||
0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x01, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, # Levels 110-11F
|
||||
0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, # Levels 120-12F
|
||||
0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, # Levels 130-13F
|
||||
0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, # Levels 140-14F
|
||||
@@ -3160,8 +3160,6 @@ def patch_rom(world: World, rom, player, active_level_dict):
|
||||
rom.write_byte(0x01BFA9, world.options.hidden_1up_checks.value)
|
||||
rom.write_byte(0x01BFAA, world.options.bonus_block_checks.value)
|
||||
rom.write_byte(0x01BFAB, world.options.blocksanity.value)
|
||||
rom.write_byte(0x01BFB7, world.options.trap_link.value)
|
||||
rom.write_byte(0x01BFB8, world.options.ring_link.value)
|
||||
|
||||
|
||||
from Utils import __version__
|
||||
|
||||
@@ -90,7 +90,6 @@ class SMWWorld(World):
|
||||
"blocksanity",
|
||||
)
|
||||
slot_data["active_levels"] = self.active_level_dict
|
||||
slot_data["trap_weights"] = self.output_trap_weights()
|
||||
|
||||
return slot_data
|
||||
|
||||
@@ -323,15 +322,3 @@ class SMWWorld(World):
|
||||
|
||||
def set_rules(self):
|
||||
set_rules(self)
|
||||
|
||||
def output_trap_weights(self) -> dict[int, int]:
|
||||
trap_data = {}
|
||||
|
||||
trap_data[0xBC0013] = self.options.ice_trap_weight.value
|
||||
trap_data[0xBC0014] = self.options.stun_trap_weight.value
|
||||
trap_data[0xBC0015] = self.options.literature_trap_weight.value
|
||||
trap_data[0xBC0016] = self.options.timer_trap_weight.value
|
||||
trap_data[0xBC001C] = self.options.reverse_trap_weight.value
|
||||
trap_data[0xBC001D] = self.options.thwimp_trap_weight.value
|
||||
|
||||
return trap_data
|
||||
|
||||
Binary file not shown.
@@ -10,13 +10,11 @@ from .bundles.bundle_room import BundleRoom
|
||||
from .bundles.bundles import get_all_bundles
|
||||
from .content import StardewContent, create_content
|
||||
from .early_items import setup_early_items
|
||||
from .items import item_table, ItemData, Group, items_by_group
|
||||
from .items.item_creation import create_items, get_all_filler_items, remove_limited_amount_packs, \
|
||||
generate_filler_choice_pool
|
||||
from .items import item_table, create_items, ItemData, Group, items_by_group, generate_filler_choice_pool
|
||||
from .locations import location_table, create_locations, LocationData, locations_by_tag
|
||||
from .logic.logic import StardewLogic
|
||||
from .options import StardewValleyOptions, SeasonRandomization, Goal, BundleRandomization, EnabledFillerBuffs, \
|
||||
NumberOfMovementBuffs, BuildingProgression, EntranceRandomization, FarmType
|
||||
from .options import StardewValleyOptions, SeasonRandomization, Goal, BundleRandomization, EnabledFillerBuffs, NumberOfMovementBuffs, \
|
||||
BuildingProgression, EntranceRandomization, FarmType
|
||||
from .options.forced_options import force_change_options_if_incompatible
|
||||
from .options.option_groups import sv_option_groups
|
||||
from .options.presets import sv_options_presets
|
||||
@@ -301,9 +299,17 @@ class StardewValleyWorld(World):
|
||||
|
||||
return StardewItem(item.name, override_classification, item.code, self.player)
|
||||
|
||||
def create_event_location(self, location_data: LocationData, rule: StardewRule, item: str):
|
||||
def create_event_location(self, location_data: LocationData, rule: StardewRule = None, item: Optional[str] = None):
|
||||
if rule is None:
|
||||
rule = True_()
|
||||
if item is None:
|
||||
item = location_data.name
|
||||
|
||||
region = self.multiworld.get_region(location_data.region, self.player)
|
||||
region.add_event(location_data.name, item, rule, StardewLocation, StardewItem)
|
||||
location = StardewLocation(self.player, location_data.name, None, region)
|
||||
location.access_rule = rule
|
||||
region.locations.append(location)
|
||||
location.place_locked_item(StardewItem(item, ItemClassification.progression, None, self.player))
|
||||
|
||||
def set_rules(self):
|
||||
set_rules(self)
|
||||
|
||||
@@ -1,26 +1,165 @@
|
||||
import csv
|
||||
import enum
|
||||
import logging
|
||||
from dataclasses import dataclass, field
|
||||
from functools import reduce
|
||||
from pathlib import Path
|
||||
from random import Random
|
||||
from typing import List, Set
|
||||
from typing import Dict, List, Protocol, Union, Set, Optional
|
||||
|
||||
from BaseClasses import Item, ItemClassification
|
||||
from .item_data import StardewItemFactory, items_by_group, Group, item_table, ItemData
|
||||
from ..content.feature import friendsanity
|
||||
from ..content.game_content import StardewContent
|
||||
from ..data.game_item import ItemTag
|
||||
from ..mods.mod_data import ModNames
|
||||
from ..options import StardewValleyOptions, FestivalLocations, ExcludeGingerIsland, SpecialOrderLocations, SeasonRandomization, Museumsanity, \
|
||||
from . import data
|
||||
from .content.feature import friendsanity
|
||||
from .content.game_content import StardewContent
|
||||
from .data.game_item import ItemTag
|
||||
from .logic.logic_event import all_events
|
||||
from .mods.mod_data import ModNames
|
||||
from .options import StardewValleyOptions, TrapItems, FestivalLocations, ExcludeGingerIsland, SpecialOrderLocations, SeasonRandomization, Museumsanity, \
|
||||
ElevatorProgression, BackpackProgression, ArcadeMachineLocations, Monstersanity, Goal, \
|
||||
Chefsanity, Craftsanity, BundleRandomization, EntranceRandomization, Shipsanity, Walnutsanity, EnabledFillerBuffs, TrapDifficulty
|
||||
from ..strings.ap_names.ap_option_names import BuffOptionName, WalnutsanityOptionName
|
||||
from ..strings.ap_names.ap_weapon_names import APWeapon
|
||||
from ..strings.ap_names.buff_names import Buff
|
||||
from ..strings.ap_names.community_upgrade_names import CommunityUpgrade
|
||||
from ..strings.ap_names.mods.mod_items import SVEQuestItem
|
||||
from ..strings.currency_names import Currency
|
||||
from ..strings.tool_names import Tool
|
||||
from ..strings.wallet_item_names import Wallet
|
||||
Chefsanity, Craftsanity, BundleRandomization, EntranceRandomization, Shipsanity, Walnutsanity, EnabledFillerBuffs
|
||||
from .strings.ap_names.ap_option_names import BuffOptionName, WalnutsanityOptionName
|
||||
from .strings.ap_names.ap_weapon_names import APWeapon
|
||||
from .strings.ap_names.buff_names import Buff
|
||||
from .strings.ap_names.community_upgrade_names import CommunityUpgrade
|
||||
from .strings.ap_names.mods.mod_items import SVEQuestItem
|
||||
from .strings.currency_names import Currency
|
||||
from .strings.tool_names import Tool
|
||||
from .strings.wallet_item_names import Wallet
|
||||
|
||||
ITEM_CODE_OFFSET = 717000
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
world_folder = Path(__file__).parent
|
||||
|
||||
|
||||
class Group(enum.Enum):
|
||||
RESOURCE_PACK = enum.auto()
|
||||
FRIENDSHIP_PACK = enum.auto()
|
||||
COMMUNITY_REWARD = enum.auto()
|
||||
TRASH = enum.auto()
|
||||
FOOTWEAR = enum.auto()
|
||||
HATS = enum.auto()
|
||||
RING = enum.auto()
|
||||
WEAPON = enum.auto()
|
||||
WEAPON_GENERIC = enum.auto()
|
||||
WEAPON_SWORD = enum.auto()
|
||||
WEAPON_CLUB = enum.auto()
|
||||
WEAPON_DAGGER = enum.auto()
|
||||
WEAPON_SLINGSHOT = enum.auto()
|
||||
PROGRESSIVE_TOOLS = enum.auto()
|
||||
SKILL_LEVEL_UP = enum.auto()
|
||||
SKILL_MASTERY = enum.auto()
|
||||
BUILDING = enum.auto()
|
||||
WIZARD_BUILDING = enum.auto()
|
||||
ARCADE_MACHINE_BUFFS = enum.auto()
|
||||
BASE_RESOURCE = enum.auto()
|
||||
WARP_TOTEM = enum.auto()
|
||||
GEODE = enum.auto()
|
||||
ORE = enum.auto()
|
||||
FERTILIZER = enum.auto()
|
||||
SEED = enum.auto()
|
||||
CROPSANITY = enum.auto()
|
||||
FISHING_RESOURCE = enum.auto()
|
||||
SEASON = enum.auto()
|
||||
TRAVELING_MERCHANT_DAY = enum.auto()
|
||||
MUSEUM = enum.auto()
|
||||
FRIENDSANITY = enum.auto()
|
||||
FESTIVAL = enum.auto()
|
||||
RARECROW = enum.auto()
|
||||
TRAP = enum.auto()
|
||||
BONUS = enum.auto()
|
||||
MAXIMUM_ONE = enum.auto()
|
||||
AT_LEAST_TWO = enum.auto()
|
||||
DEPRECATED = enum.auto()
|
||||
RESOURCE_PACK_USEFUL = enum.auto()
|
||||
SPECIAL_ORDER_BOARD = enum.auto()
|
||||
SPECIAL_ORDER_QI = enum.auto()
|
||||
BABY = enum.auto()
|
||||
GINGER_ISLAND = enum.auto()
|
||||
WALNUT_PURCHASE = enum.auto()
|
||||
TV_CHANNEL = enum.auto()
|
||||
QI_CRAFTING_RECIPE = enum.auto()
|
||||
CHEFSANITY = enum.auto()
|
||||
CHEFSANITY_STARTER = enum.auto()
|
||||
CHEFSANITY_QOS = enum.auto()
|
||||
CHEFSANITY_PURCHASE = enum.auto()
|
||||
CHEFSANITY_FRIENDSHIP = enum.auto()
|
||||
CHEFSANITY_SKILL = enum.auto()
|
||||
CRAFTSANITY = enum.auto()
|
||||
BOOK_POWER = enum.auto()
|
||||
LOST_BOOK = enum.auto()
|
||||
PLAYER_BUFF = enum.auto()
|
||||
# Mods
|
||||
MAGIC_SPELL = enum.auto()
|
||||
MOD_WARP = enum.auto()
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ItemData:
|
||||
code_without_offset: Optional[int]
|
||||
name: str
|
||||
classification: ItemClassification
|
||||
mod_name: Optional[str] = None
|
||||
groups: Set[Group] = field(default_factory=frozenset)
|
||||
|
||||
def __post_init__(self):
|
||||
if not isinstance(self.groups, frozenset):
|
||||
super().__setattr__("groups", frozenset(self.groups))
|
||||
|
||||
@property
|
||||
def code(self):
|
||||
return ITEM_CODE_OFFSET + self.code_without_offset if self.code_without_offset is not None else None
|
||||
|
||||
def has_any_group(self, *group: Group) -> bool:
|
||||
groups = set(group)
|
||||
return bool(groups.intersection(self.groups))
|
||||
|
||||
|
||||
class StardewItemFactory(Protocol):
|
||||
def __call__(self, name: Union[str, ItemData], override_classification: ItemClassification = None) -> Item:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def load_item_csv():
|
||||
from importlib.resources import files
|
||||
|
||||
items = []
|
||||
with files(data).joinpath("items.csv").open() as file:
|
||||
item_reader = csv.DictReader(file)
|
||||
for item in item_reader:
|
||||
id = int(item["id"]) if item["id"] else None
|
||||
classification = reduce((lambda a, b: a | b), {ItemClassification[str_classification] for str_classification in item["classification"].split(",")})
|
||||
groups = {Group[group] for group in item["groups"].split(",") if group}
|
||||
mod_name = str(item["mod_name"]) if item["mod_name"] else None
|
||||
items.append(ItemData(id, item["name"], classification, mod_name, groups))
|
||||
return items
|
||||
|
||||
|
||||
events = [
|
||||
ItemData(None, e, ItemClassification.progression)
|
||||
for e in sorted(all_events)
|
||||
]
|
||||
|
||||
all_items: List[ItemData] = load_item_csv() + events
|
||||
item_table: Dict[str, ItemData] = {}
|
||||
items_by_group: Dict[Group, List[ItemData]] = {}
|
||||
|
||||
|
||||
def initialize_groups():
|
||||
for item in all_items:
|
||||
for group in item.groups:
|
||||
item_group = items_by_group.get(group, list())
|
||||
item_group.append(item)
|
||||
items_by_group[group] = item_group
|
||||
|
||||
|
||||
def initialize_item_table():
|
||||
item_table.update({item.name: item for item in all_items})
|
||||
|
||||
|
||||
initialize_item_table()
|
||||
initialize_groups()
|
||||
|
||||
|
||||
def get_too_many_items_error_message(locations_count: int, items_count: int) -> str:
|
||||
return f"There should be at least as many locations [{locations_count}] as there are mandatory items [{items_count}]"
|
||||
@@ -573,15 +712,13 @@ def weapons_count(options: StardewValleyOptions):
|
||||
def fill_with_resource_packs_and_traps(item_factory: StardewItemFactory, options: StardewValleyOptions, random: Random,
|
||||
items_already_added: List[Item],
|
||||
available_item_slots: int) -> List[Item]:
|
||||
include_traps = options.trap_difficulty != TrapDifficulty.option_no_traps
|
||||
include_traps = options.trap_items != TrapItems.option_no_traps
|
||||
items_already_added_names = [item.name for item in items_already_added]
|
||||
useful_resource_packs = [pack for pack in items_by_group[Group.RESOURCE_PACK_USEFUL]
|
||||
if pack.name not in items_already_added_names]
|
||||
trap_items = [trap for trap in items_by_group[Group.TRAP]
|
||||
if trap.name not in items_already_added_names and
|
||||
Group.DEPRECATED not in trap.groups and
|
||||
(trap.mod_name is None or trap.mod_name in options.mods) and
|
||||
options.trap_distribution[trap.name] > 0]
|
||||
(trap.mod_name is None or trap.mod_name in options.mods)]
|
||||
player_buffs = get_allowed_player_buffs(options.enabled_filler_buffs)
|
||||
|
||||
priority_filler_items = []
|
||||
@@ -613,13 +750,11 @@ def fill_with_resource_packs_and_traps(item_factory: StardewItemFactory, options
|
||||
(filler_pack.name not in [priority_item.name for priority_item in
|
||||
priority_filler_items] and filler_pack.name not in items_already_added_names)]
|
||||
|
||||
filler_weights = get_filler_weights(options, all_filler_packs)
|
||||
|
||||
while available_item_slots > 0:
|
||||
resource_pack = random.choices(all_filler_packs, weights=filler_weights, k=1)[0]
|
||||
resource_pack = random.choice(all_filler_packs)
|
||||
exactly_2 = Group.AT_LEAST_TWO in resource_pack.groups
|
||||
while exactly_2 and available_item_slots == 1:
|
||||
resource_pack = random.choices(all_filler_packs, weights=filler_weights, k=1)[0]
|
||||
resource_pack = random.choice(all_filler_packs)
|
||||
exactly_2 = Group.AT_LEAST_TWO in resource_pack.groups
|
||||
classification = ItemClassification.useful if resource_pack.classification == ItemClassification.progression else resource_pack.classification
|
||||
items.append(item_factory(resource_pack, classification))
|
||||
@@ -628,24 +763,11 @@ def fill_with_resource_packs_and_traps(item_factory: StardewItemFactory, options
|
||||
items.append(item_factory(resource_pack, classification))
|
||||
available_item_slots -= 1
|
||||
if exactly_2 or Group.MAXIMUM_ONE in resource_pack.groups:
|
||||
index = all_filler_packs.index(resource_pack)
|
||||
all_filler_packs.pop(index)
|
||||
filler_weights.pop(index)
|
||||
all_filler_packs.remove(resource_pack)
|
||||
|
||||
return items
|
||||
|
||||
|
||||
def get_filler_weights(options: StardewValleyOptions, all_filler_packs: List[ItemData]):
|
||||
weights = []
|
||||
for filler in all_filler_packs:
|
||||
if filler.name in options.trap_distribution:
|
||||
num = options.trap_distribution[filler.name]
|
||||
else:
|
||||
num = options.trap_distribution.default_weight
|
||||
weights.append(num)
|
||||
return weights
|
||||
|
||||
|
||||
def filter_deprecated_items(items: List[ItemData]) -> List[ItemData]:
|
||||
return [item for item in items if Group.DEPRECATED not in item.groups]
|
||||
|
||||
@@ -670,7 +792,7 @@ def remove_excluded_items_island_mods(items, exclude_ginger_island: bool, mods:
|
||||
|
||||
|
||||
def generate_filler_choice_pool(options: StardewValleyOptions) -> list[str]:
|
||||
include_traps = options.trap_difficulty != TrapDifficulty.option_no_traps
|
||||
include_traps = options.trap_items != TrapItems.option_no_traps
|
||||
exclude_island = options.exclude_ginger_island == ExcludeGingerIsland.option_true
|
||||
|
||||
available_filler = get_all_filler_items(include_traps, exclude_island)
|
||||
@@ -1 +0,0 @@
|
||||
from .item_data import item_table, ItemData, Group, items_by_group, load_item_csv
|
||||
@@ -1,143 +0,0 @@
|
||||
import csv
|
||||
import enum
|
||||
from dataclasses import dataclass, field
|
||||
from functools import reduce
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Protocol, Union, Set, Optional
|
||||
|
||||
from BaseClasses import Item, ItemClassification
|
||||
from .. import data
|
||||
from ..logic.logic_event import all_events
|
||||
|
||||
ITEM_CODE_OFFSET = 717000
|
||||
|
||||
world_folder = Path(__file__).parent
|
||||
|
||||
|
||||
class Group(enum.Enum):
|
||||
RESOURCE_PACK = enum.auto()
|
||||
FRIENDSHIP_PACK = enum.auto()
|
||||
COMMUNITY_REWARD = enum.auto()
|
||||
TRASH = enum.auto()
|
||||
FOOTWEAR = enum.auto()
|
||||
HATS = enum.auto()
|
||||
RING = enum.auto()
|
||||
WEAPON = enum.auto()
|
||||
WEAPON_GENERIC = enum.auto()
|
||||
WEAPON_SWORD = enum.auto()
|
||||
WEAPON_CLUB = enum.auto()
|
||||
WEAPON_DAGGER = enum.auto()
|
||||
WEAPON_SLINGSHOT = enum.auto()
|
||||
PROGRESSIVE_TOOLS = enum.auto()
|
||||
SKILL_LEVEL_UP = enum.auto()
|
||||
SKILL_MASTERY = enum.auto()
|
||||
BUILDING = enum.auto()
|
||||
WIZARD_BUILDING = enum.auto()
|
||||
ARCADE_MACHINE_BUFFS = enum.auto()
|
||||
BASE_RESOURCE = enum.auto()
|
||||
WARP_TOTEM = enum.auto()
|
||||
GEODE = enum.auto()
|
||||
ORE = enum.auto()
|
||||
FERTILIZER = enum.auto()
|
||||
SEED = enum.auto()
|
||||
CROPSANITY = enum.auto()
|
||||
FISHING_RESOURCE = enum.auto()
|
||||
SEASON = enum.auto()
|
||||
TRAVELING_MERCHANT_DAY = enum.auto()
|
||||
MUSEUM = enum.auto()
|
||||
FRIENDSANITY = enum.auto()
|
||||
FESTIVAL = enum.auto()
|
||||
RARECROW = enum.auto()
|
||||
TRAP = enum.auto()
|
||||
BONUS = enum.auto()
|
||||
MAXIMUM_ONE = enum.auto()
|
||||
AT_LEAST_TWO = enum.auto()
|
||||
DEPRECATED = enum.auto()
|
||||
RESOURCE_PACK_USEFUL = enum.auto()
|
||||
SPECIAL_ORDER_BOARD = enum.auto()
|
||||
SPECIAL_ORDER_QI = enum.auto()
|
||||
BABY = enum.auto()
|
||||
GINGER_ISLAND = enum.auto()
|
||||
WALNUT_PURCHASE = enum.auto()
|
||||
TV_CHANNEL = enum.auto()
|
||||
QI_CRAFTING_RECIPE = enum.auto()
|
||||
CHEFSANITY = enum.auto()
|
||||
CHEFSANITY_STARTER = enum.auto()
|
||||
CHEFSANITY_QOS = enum.auto()
|
||||
CHEFSANITY_PURCHASE = enum.auto()
|
||||
CHEFSANITY_FRIENDSHIP = enum.auto()
|
||||
CHEFSANITY_SKILL = enum.auto()
|
||||
CRAFTSANITY = enum.auto()
|
||||
BOOK_POWER = enum.auto()
|
||||
LOST_BOOK = enum.auto()
|
||||
PLAYER_BUFF = enum.auto()
|
||||
# Mods
|
||||
MAGIC_SPELL = enum.auto()
|
||||
MOD_WARP = enum.auto()
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ItemData:
|
||||
code_without_offset: Optional[int]
|
||||
name: str
|
||||
classification: ItemClassification
|
||||
mod_name: Optional[str] = None
|
||||
groups: Set[Group] = field(default_factory=frozenset)
|
||||
|
||||
def __post_init__(self):
|
||||
if not isinstance(self.groups, frozenset):
|
||||
super().__setattr__("groups", frozenset(self.groups))
|
||||
|
||||
@property
|
||||
def code(self):
|
||||
return ITEM_CODE_OFFSET + self.code_without_offset if self.code_without_offset is not None else None
|
||||
|
||||
def has_any_group(self, *group: Group) -> bool:
|
||||
groups = set(group)
|
||||
return bool(groups.intersection(self.groups))
|
||||
|
||||
|
||||
class StardewItemFactory(Protocol):
|
||||
def __call__(self, name: Union[str, ItemData], override_classification: ItemClassification = None) -> Item:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def load_item_csv():
|
||||
from importlib.resources import files
|
||||
|
||||
items = []
|
||||
with files(data).joinpath("items.csv").open() as file:
|
||||
item_reader = csv.DictReader(file)
|
||||
for item in item_reader:
|
||||
id = int(item["id"]) if item["id"] else None
|
||||
classification = reduce((lambda a, b: a | b), {ItemClassification[str_classification] for str_classification in item["classification"].split(",")})
|
||||
groups = {Group[group] for group in item["groups"].split(",") if group}
|
||||
mod_name = str(item["mod_name"]) if item["mod_name"] else None
|
||||
items.append(ItemData(id, item["name"], classification, mod_name, groups))
|
||||
return items
|
||||
|
||||
|
||||
events = [
|
||||
ItemData(None, e, ItemClassification.progression)
|
||||
for e in sorted(all_events)
|
||||
]
|
||||
|
||||
all_items: List[ItemData] = load_item_csv() + events
|
||||
item_table: Dict[str, ItemData] = {}
|
||||
items_by_group: Dict[Group, List[ItemData]] = {}
|
||||
|
||||
|
||||
def initialize_groups():
|
||||
for item in all_items:
|
||||
for group in item.groups:
|
||||
item_group = items_by_group.get(group, list())
|
||||
item_group.append(item)
|
||||
items_by_group[group] = item_group
|
||||
|
||||
|
||||
def initialize_item_table():
|
||||
item_table.update({item.name: item for item in all_items})
|
||||
|
||||
|
||||
initialize_item_table()
|
||||
initialize_groups()
|
||||
@@ -154,7 +154,7 @@ class FestivalLogic(BaseLogic):
|
||||
# Salads at the bar are good enough
|
||||
cooking_rule = self.logic.money.can_spend_at(Region.saloon, 220)
|
||||
|
||||
fish_rule = self.logic.fishing.can_fish_anywhere(50)
|
||||
fish_rule = self.logic.skill.can_fish(difficulty=50)
|
||||
|
||||
# Hazelnut always available since the grange display is in fall
|
||||
forage_rule = self.logic.region.can_reach_any((Region.forest, Region.backwoods))
|
||||
@@ -179,7 +179,7 @@ class FestivalLogic(BaseLogic):
|
||||
return animal_rule & artisan_rule & cooking_rule & fish_rule & forage_rule & fruit_rule & mineral_rule & vegetable_rule
|
||||
|
||||
def can_win_fishing_competition(self) -> StardewRule:
|
||||
return self.logic.fishing.can_fish(60)
|
||||
return self.logic.skill.can_fish(difficulty=60)
|
||||
|
||||
def has_all_rarecrows(self) -> StardewRule:
|
||||
rules = []
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
from functools import cached_property
|
||||
|
||||
from Utils import cache_self1
|
||||
from .base_logic import BaseLogicMixin, BaseLogic
|
||||
from ..data import fish_data
|
||||
@@ -14,8 +12,6 @@ from ..strings.quality_names import FishQuality
|
||||
from ..strings.region_names import Region
|
||||
from ..strings.skill_names import Skill
|
||||
|
||||
fishing_regions = (Region.beach, Region.town, Region.forest, Region.mountain, Region.island_south, Region.island_west)
|
||||
|
||||
|
||||
class FishingLogicMixin(BaseLogicMixin):
|
||||
def __init__(self, *args, **kwargs):
|
||||
@@ -24,35 +20,17 @@ class FishingLogicMixin(BaseLogicMixin):
|
||||
|
||||
|
||||
class FishingLogic(BaseLogic):
|
||||
@cache_self1
|
||||
def can_fish_anywhere(self, difficulty: int = 0) -> StardewRule:
|
||||
return self.logic.fishing.can_fish(difficulty) & self.logic.region.can_reach_any(fishing_regions)
|
||||
|
||||
def can_fish_in_freshwater(self) -> StardewRule:
|
||||
return self.logic.fishing.can_fish() & self.logic.region.can_reach_any((Region.forest, Region.town, Region.mountain))
|
||||
return self.logic.skill.can_fish() & self.logic.region.can_reach_any((Region.forest, Region.town, Region.mountain))
|
||||
|
||||
@cached_property
|
||||
def has_max_fishing(self) -> StardewRule:
|
||||
return self.logic.tool.has_fishing_rod(4) & self.logic.skill.has_level(Skill.fishing, 10)
|
||||
|
||||
@cached_property
|
||||
def can_fish_chests(self) -> StardewRule:
|
||||
return self.logic.tool.has_fishing_rod(4) & self.logic.skill.has_level(Skill.fishing, 6)
|
||||
|
||||
@cache_self1
|
||||
def can_fish_at(self, region: str) -> StardewRule:
|
||||
return self.logic.fishing.can_fish() & self.logic.region.can_reach(region)
|
||||
|
||||
@cache_self1
|
||||
def can_fish(self, difficulty: int = 0) -> StardewRule:
|
||||
skill_required = min(10, max(0, int((difficulty / 10) - 1)))
|
||||
if difficulty <= 40:
|
||||
skill_required = 0
|
||||
|
||||
skill_rule = self.logic.skill.has_level(Skill.fishing, skill_required)
|
||||
# Training rod only works with fish < 50. Fiberglass does not help you to catch higher difficulty fish, so it's skipped in logic.
|
||||
number_fishing_rod_required = 1 if difficulty < 50 else (2 if difficulty < 80 else 4)
|
||||
return self.logic.tool.has_fishing_rod(number_fishing_rod_required) & skill_rule
|
||||
return self.logic.skill.can_fish() & self.logic.region.can_reach(region)
|
||||
|
||||
@cache_self1
|
||||
def can_catch_fish(self, fish: FishItem) -> StardewRule:
|
||||
@@ -61,17 +39,14 @@ class FishingLogic(BaseLogic):
|
||||
quest_rule = self.logic.fishing.can_start_extended_family_quest()
|
||||
region_rule = self.logic.region.can_reach_any(fish.locations)
|
||||
season_rule = self.logic.season.has_any(fish.seasons)
|
||||
|
||||
if fish.difficulty == -1:
|
||||
difficulty_rule = self.logic.fishing.can_crab_pot
|
||||
difficulty_rule = self.logic.skill.can_crab_pot
|
||||
else:
|
||||
difficulty_rule = self.logic.fishing.can_fish(120 if fish.legendary else fish.difficulty)
|
||||
|
||||
difficulty_rule = self.logic.skill.can_fish(difficulty=(120 if fish.legendary else fish.difficulty))
|
||||
if fish.name == SVEFish.kittyfish:
|
||||
item_rule = self.logic.received(SVEQuestItem.kittyfish_spell)
|
||||
else:
|
||||
item_rule = True_()
|
||||
|
||||
return quest_rule & region_rule & season_rule & difficulty_rule & item_rule
|
||||
|
||||
def can_catch_fish_for_fishsanity(self, fish: FishItem) -> StardewRule:
|
||||
@@ -103,7 +78,7 @@ class FishingLogic(BaseLogic):
|
||||
return self.logic.tool.has_fishing_rod(4) & self.logic.has(tackle)
|
||||
|
||||
def can_catch_every_fish(self) -> StardewRule:
|
||||
rules = [self.has_max_fishing]
|
||||
rules = [self.has_max_fishing()]
|
||||
|
||||
rules.extend(
|
||||
self.logic.fishing.can_catch_fish(fish)
|
||||
@@ -114,23 +89,3 @@ class FishingLogic(BaseLogic):
|
||||
|
||||
def has_specific_bait(self, fish: FishItem) -> StardewRule:
|
||||
return self.can_catch_fish(fish) & self.logic.has(Machine.bait_maker)
|
||||
|
||||
@cached_property
|
||||
def can_crab_pot_anywhere(self) -> StardewRule:
|
||||
return self.logic.fishing.can_fish() & self.logic.region.can_reach_any(fishing_regions)
|
||||
|
||||
@cache_self1
|
||||
def can_crab_pot_at(self, region: str) -> StardewRule:
|
||||
return self.logic.fishing.can_crab_pot & self.logic.region.can_reach(region)
|
||||
|
||||
@cached_property
|
||||
def can_crab_pot(self) -> StardewRule:
|
||||
crab_pot_rule = self.logic.has(Fishing.bait)
|
||||
|
||||
# We can't use the same rule if skills are vanilla, because fishing levels are required to crab pot, which is required to get fishing levels...
|
||||
if self.content.features.skill_progression.is_progressive:
|
||||
crab_pot_rule = crab_pot_rule & self.logic.has(Machine.crab_pot)
|
||||
else:
|
||||
crab_pot_rule = crab_pot_rule & self.logic.skill.can_get_fishing_xp
|
||||
|
||||
return crab_pot_rule
|
||||
|
||||
@@ -60,7 +60,7 @@ class GoalLogic(BaseLogic):
|
||||
if not self.content.features.fishsanity.is_enabled:
|
||||
return self.logic.fishing.can_catch_every_fish()
|
||||
|
||||
rules = [self.logic.fishing.has_max_fishing]
|
||||
rules = [self.logic.fishing.has_max_fishing()]
|
||||
|
||||
rules.extend(
|
||||
self.logic.fishing.can_catch_fish_for_fishsanity(fish)
|
||||
|
||||
@@ -130,9 +130,9 @@ class StardewLogic(ReceivedLogicMixin, HasLogicMixin, RegionLogicMixin, Travelin
|
||||
# @formatter:off
|
||||
self.registry.item_rules.update({
|
||||
"Energy Tonic": self.money.can_spend_at(Region.hospital, 1000),
|
||||
WaterChest.fishing_chest: self.fishing.can_fish_chests,
|
||||
WaterChest.golden_fishing_chest: self.fishing.can_fish_chests & self.skill.has_mastery(Skill.fishing),
|
||||
WaterChest.treasure: self.fishing.can_fish_chests,
|
||||
WaterChest.fishing_chest: self.fishing.can_fish_chests(),
|
||||
WaterChest.golden_fishing_chest: self.fishing.can_fish_chests() & self.skill.has_mastery(Skill.fishing),
|
||||
WaterChest.treasure: self.fishing.can_fish_chests(),
|
||||
Ring.hot_java_ring: self.region.can_reach(Region.volcano_floor_10),
|
||||
"Galaxy Soul": self.money.can_trade_at(Region.qi_walnut_room, Currency.qi_gem, 40),
|
||||
"JotPK Big Buff": self.arcade.has_jotpk_power_level(7),
|
||||
@@ -164,7 +164,7 @@ class StardewLogic(ReceivedLogicMixin, HasLogicMixin, RegionLogicMixin, Travelin
|
||||
AnimalProduct.large_milk: self.animal.has_happy_animal(Animal.cow),
|
||||
AnimalProduct.milk: self.animal.has_animal(Animal.cow),
|
||||
AnimalProduct.rabbit_foot: self.animal.has_happy_animal(Animal.rabbit),
|
||||
AnimalProduct.roe: self.fishing.can_fish_anywhere() & self.building.has_building(Building.fish_pond),
|
||||
AnimalProduct.roe: self.skill.can_fish() & self.building.has_building(Building.fish_pond),
|
||||
AnimalProduct.squid_ink: self.mine.can_mine_in_the_mines_floor_81_120() | (self.building.has_building(Building.fish_pond) & self.has(Fish.squid)),
|
||||
AnimalProduct.sturgeon_roe: self.has(Fish.sturgeon) & self.building.has_building(Building.fish_pond),
|
||||
AnimalProduct.truffle: self.animal.has_animal(Animal.pig) & self.season.has_any_not_winter(),
|
||||
@@ -198,7 +198,7 @@ class StardewLogic(ReceivedLogicMixin, HasLogicMixin, RegionLogicMixin, Travelin
|
||||
ArtisanGood.targeted_bait: self.artisan.has_targeted_bait(),
|
||||
ArtisanGood.stardrop_tea: self.has(WaterChest.golden_fishing_chest),
|
||||
ArtisanGood.truffle_oil: self.has(AnimalProduct.truffle) & self.has(Machine.oil_maker),
|
||||
ArtisanGood.void_mayonnaise: self.artisan.can_mayonnaise(AnimalProduct.void_egg),
|
||||
ArtisanGood.void_mayonnaise: (self.skill.can_fish(Region.witch_swamp)) | (self.artisan.can_mayonnaise(AnimalProduct.void_egg)),
|
||||
Beverage.pina_colada: self.money.can_spend_at(Region.island_resort, 600),
|
||||
Beverage.triple_shot_espresso: self.has("Hot Java Ring"),
|
||||
Consumable.butterfly_powder: self.money.can_spend_at(Region.sewer, 20000),
|
||||
@@ -217,15 +217,15 @@ class StardewLogic(ReceivedLogicMixin, HasLogicMixin, RegionLogicMixin, Travelin
|
||||
Fertilizer.quality: self.time.has_year_two & self.money.can_spend_at(Region.pierre_store, 150),
|
||||
Fertilizer.tree: self.skill.has_level(Skill.foraging, 7) & self.has(Material.fiber) & self.has(Material.stone),
|
||||
Fish.any: self.logic.or_(*(self.fishing.can_catch_fish(fish) for fish in content.fishes.values())),
|
||||
Fish.crab: self.fishing.can_crab_pot_at(Region.beach),
|
||||
Fish.crayfish: self.fishing.can_crab_pot_at(Region.town),
|
||||
Fish.lobster: self.fishing.can_crab_pot_at(Region.beach),
|
||||
Fish.crab: self.skill.can_crab_pot_at(Region.beach),
|
||||
Fish.crayfish: self.skill.can_crab_pot_at(Region.town),
|
||||
Fish.lobster: self.skill.can_crab_pot_at(Region.beach),
|
||||
Fish.mussel: self.tool.can_forage(Generic.any, Region.beach) or self.has(Fish.mussel_node),
|
||||
Fish.mussel_node: self.region.can_reach(Region.island_west),
|
||||
Fish.oyster: self.tool.can_forage(Generic.any, Region.beach),
|
||||
Fish.periwinkle: self.fishing.can_crab_pot_at(Region.town),
|
||||
Fish.shrimp: self.fishing.can_crab_pot_at(Region.beach),
|
||||
Fish.snail: self.fishing.can_crab_pot_at(Region.town),
|
||||
Fish.periwinkle: self.skill.can_crab_pot_at(Region.town),
|
||||
Fish.shrimp: self.skill.can_crab_pot_at(Region.beach),
|
||||
Fish.snail: self.skill.can_crab_pot_at(Region.town),
|
||||
Fishing.curiosity_lure: self.monster.can_kill(self.monster.all_monsters_by_name[Monster.mummy]),
|
||||
Fishing.lead_bobber: self.skill.has_level(Skill.fishing, 6) & self.money.can_spend_at(Region.fish_shop, 200),
|
||||
Forageable.hay: self.building.has_building(Building.silo) & self.tool.has_tool(Tool.scythe), #
|
||||
@@ -235,7 +235,7 @@ class StardewLogic(ReceivedLogicMixin, HasLogicMixin, RegionLogicMixin, Travelin
|
||||
Fossil.fossilized_leg: self.region.can_reach(Region.dig_site) & self.tool.has_tool(Tool.pickaxe),
|
||||
Fossil.fossilized_ribs: self.region.can_reach(Region.island_south) & self.tool.has_tool(Tool.hoe),
|
||||
Fossil.fossilized_skull: self.action.can_open_geode(Geode.golden_coconut),
|
||||
Fossil.fossilized_spine: self.fishing.can_fish_at(Region.dig_site),
|
||||
Fossil.fossilized_spine: self.skill.can_fish(Region.dig_site),
|
||||
Fossil.fossilized_tail: self.action.can_pan_at(Region.dig_site, ToolMaterial.copper),
|
||||
Fossil.mummified_bat: self.region.can_reach(Region.volcano_floor_10),
|
||||
Fossil.mummified_frog: self.region.can_reach(Region.island_east) & self.tool.has_tool(Tool.scythe),
|
||||
@@ -296,12 +296,12 @@ class StardewLogic(ReceivedLogicMixin, HasLogicMixin, RegionLogicMixin, Travelin
|
||||
RetainingSoil.quality: self.time.has_year_two & self.money.can_spend_at(Region.pierre_store, 150),
|
||||
SpeedGro.basic: self.money.can_spend_at(Region.pierre_store, 100),
|
||||
SpeedGro.deluxe: self.time.has_year_two & self.money.can_spend_at(Region.pierre_store, 150),
|
||||
Trash.broken_cd: self.fishing.can_crab_pot_anywhere,
|
||||
Trash.broken_glasses: self.fishing.can_crab_pot_anywhere,
|
||||
Trash.driftwood: self.fishing.can_crab_pot_anywhere,
|
||||
Trash.broken_cd: self.skill.can_crab_pot,
|
||||
Trash.broken_glasses: self.skill.can_crab_pot,
|
||||
Trash.driftwood: self.skill.can_crab_pot,
|
||||
Trash.joja_cola: self.money.can_spend_at(Region.saloon, 75),
|
||||
Trash.soggy_newspaper: self.fishing.can_crab_pot_anywhere,
|
||||
Trash.trash: self.fishing.can_crab_pot_anywhere,
|
||||
Trash.soggy_newspaper: self.skill.can_crab_pot,
|
||||
Trash.trash: self.skill.can_crab_pot,
|
||||
TreeSeed.acorn: self.skill.has_level(Skill.foraging, 1) & self.ability.can_chop_trees(),
|
||||
TreeSeed.mahogany: self.region.can_reach(Region.secret_woods) & self.tool.has_tool(Tool.axe, ToolMaterial.iron) & self.skill.has_level(Skill.foraging, 1),
|
||||
TreeSeed.maple: self.skill.has_level(Skill.foraging, 1) & self.ability.can_chop_trees(),
|
||||
@@ -314,8 +314,8 @@ class StardewLogic(ReceivedLogicMixin, HasLogicMixin, RegionLogicMixin, Travelin
|
||||
WaterItem.cave_jelly: self.fishing.can_fish_at(Region.mines_floor_100) & self.tool.has_fishing_rod(2),
|
||||
WaterItem.river_jelly: self.fishing.can_fish_at(Region.town) & self.tool.has_fishing_rod(2),
|
||||
WaterItem.sea_jelly: self.fishing.can_fish_at(Region.beach) & self.tool.has_fishing_rod(2),
|
||||
WaterItem.seaweed: self.fishing.can_fish_at(Region.tide_pools),
|
||||
WaterItem.white_algae: self.fishing.can_fish_at(Region.mines_floor_20),
|
||||
WaterItem.seaweed: self.skill.can_fish(Region.tide_pools),
|
||||
WaterItem.white_algae: self.skill.can_fish(Region.mines_floor_20),
|
||||
WildSeeds.grass_starter: self.money.can_spend_at(Region.pierre_store, 100),
|
||||
})
|
||||
# @formatter:on
|
||||
|
||||
@@ -39,7 +39,7 @@ class QuestLogic(BaseLogic):
|
||||
Quest.raising_animals: self.logic.quest.can_complete_quest(Quest.getting_started) & self.logic.building.has_building(Building.coop),
|
||||
Quest.feeding_animals: self.logic.quest.can_complete_quest(Quest.getting_started) & self.logic.building.has_building(Building.silo),
|
||||
Quest.advancement: self.logic.quest.can_complete_quest(Quest.getting_started) & self.logic.has(Craftable.scarecrow),
|
||||
Quest.archaeology: self.logic.tool.has_tool(Tool.hoe) | self.logic.mine.can_mine_in_the_mines_floor_1_40() | self.logic.fishing.can_fish_chests,
|
||||
Quest.archaeology: self.logic.tool.has_tool(Tool.hoe) | self.logic.mine.can_mine_in_the_mines_floor_1_40() | self.logic.skill.can_fish(),
|
||||
Quest.rat_problem: self.logic.region.can_reach_all((Region.town, Region.community_center)),
|
||||
Quest.meet_the_wizard: self.logic.quest.can_complete_quest(Quest.rat_problem),
|
||||
Quest.forging_ahead: self.logic.has(Ore.copper) & self.logic.has(Machine.furnace),
|
||||
@@ -86,9 +86,7 @@ class QuestLogic(BaseLogic):
|
||||
Quest.catch_a_lingcod: self.logic.season.has(Season.winter) & self.logic.has(Fish.lingcod) & self.logic.relationship.can_meet(NPC.willy),
|
||||
Quest.dark_talisman: self.logic.region.can_reach(Region.railroad) & self.logic.wallet.has_rusty_key() & self.logic.relationship.can_meet(
|
||||
NPC.krobus),
|
||||
Quest.goblin_problem: self.logic.region.can_reach(Region.witch_swamp)
|
||||
# Void mayo can be fished at 5% chance in the witch swamp while the quest is active. It drops a lot after the quest.
|
||||
& (self.logic.has(ArtisanGood.void_mayonnaise) | self.logic.fishing.can_fish()),
|
||||
Quest.goblin_problem: self.logic.region.can_reach(Region.witch_swamp),
|
||||
Quest.magic_ink: self.logic.relationship.can_meet(NPC.wizard),
|
||||
Quest.the_pirates_wife: self.logic.relationship.can_meet(NPC.kent) & self.logic.relationship.can_meet(NPC.gus) &
|
||||
self.logic.relationship.can_meet(NPC.sandy) & self.logic.relationship.can_meet(NPC.george) &
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
from functools import cached_property
|
||||
from typing import Union, Tuple
|
||||
|
||||
from Utils import cache_self1
|
||||
from .base_logic import BaseLogicMixin, BaseLogic
|
||||
from ..data.harvest import HarvestCropSource
|
||||
from ..mods.logic.mod_skills_levels import get_mod_skill_levels
|
||||
from ..stardew_rule import StardewRule, true_, True_, False_
|
||||
from ..strings.craftable_names import Fishing
|
||||
from ..strings.machine_names import Machine
|
||||
from ..strings.performance_names import Performance
|
||||
from ..strings.quality_names import ForageQuality
|
||||
from ..strings.region_names import Region
|
||||
@@ -12,6 +15,7 @@ from ..strings.skill_names import Skill, all_mod_skills, all_vanilla_skills
|
||||
from ..strings.tool_names import ToolMaterial, Tool
|
||||
from ..strings.wallet_item_names import Wallet
|
||||
|
||||
fishing_regions = (Region.beach, Region.town, Region.forest, Region.mountain, Region.island_south, Region.island_west)
|
||||
vanilla_skill_items = ("Farming Level", "Mining Level", "Foraging Level", "Fishing Level", "Combat Level")
|
||||
|
||||
|
||||
@@ -134,9 +138,44 @@ class SkillLogic(BaseLogic):
|
||||
@cached_property
|
||||
def can_get_fishing_xp(self) -> StardewRule:
|
||||
if self.content.features.skill_progression.is_progressive:
|
||||
return self.logic.fishing.can_fish_anywhere() | self.logic.fishing.can_crab_pot
|
||||
return self.logic.skill.can_fish() | self.logic.skill.can_crab_pot
|
||||
|
||||
return self.logic.fishing.can_fish_anywhere()
|
||||
return self.logic.skill.can_fish()
|
||||
|
||||
# Should be cached
|
||||
def can_fish(self, regions: Union[str, Tuple[str, ...]] = None, difficulty: int = 0) -> StardewRule:
|
||||
if isinstance(regions, str):
|
||||
regions = regions,
|
||||
|
||||
if regions is None or len(regions) == 0:
|
||||
regions = fishing_regions
|
||||
|
||||
skill_required = min(10, max(0, int((difficulty / 10) - 1)))
|
||||
if difficulty <= 40:
|
||||
skill_required = 0
|
||||
|
||||
skill_rule = self.logic.skill.has_level(Skill.fishing, skill_required)
|
||||
region_rule = self.logic.region.can_reach_any(regions)
|
||||
# Training rod only works with fish < 50. Fiberglass does not help you to catch higher difficulty fish, so it's skipped in logic.
|
||||
number_fishing_rod_required = 1 if difficulty < 50 else (2 if difficulty < 80 else 4)
|
||||
return self.logic.tool.has_fishing_rod(number_fishing_rod_required) & skill_rule & region_rule
|
||||
|
||||
@cache_self1
|
||||
def can_crab_pot_at(self, region: str) -> StardewRule:
|
||||
return self.logic.skill.can_crab_pot & self.logic.region.can_reach(region)
|
||||
|
||||
@cached_property
|
||||
def can_crab_pot(self) -> StardewRule:
|
||||
crab_pot_rule = self.logic.has(Fishing.bait)
|
||||
|
||||
# We can't use the same rule if skills are vanilla, because fishing levels are required to crab pot, which is required to get fishing levels...
|
||||
if self.content.features.skill_progression.is_progressive:
|
||||
crab_pot_rule = crab_pot_rule & self.logic.has(Machine.crab_pot)
|
||||
else:
|
||||
crab_pot_rule = crab_pot_rule & self.logic.skill.can_get_fishing_xp
|
||||
|
||||
water_region_rules = self.logic.region.can_reach_any(fishing_regions)
|
||||
return crab_pot_rule & water_region_rules
|
||||
|
||||
def can_forage_quality(self, quality: str) -> StardewRule:
|
||||
if quality == ForageQuality.basic:
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user