mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-03-19 22:08:21 -07:00
Compare commits
22 Commits
oc2-progre
...
0.6.2-rc1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b2d2c8e596 | ||
|
|
68e37b8f9a | ||
|
|
fa2d7797f4 | ||
|
|
1885dab066 | ||
|
|
9425f5b772 | ||
|
|
83ed3c8b50 | ||
|
|
f4690e296d | ||
|
|
68c350b4c0 | ||
|
|
da0207f5cb | ||
|
|
2455f1158f | ||
|
|
1031fc4923 | ||
|
|
6beaacb905 | ||
|
|
c46ee7c420 | ||
|
|
227f0bce3d | ||
|
|
611e1c2b19 | ||
|
|
5f974b7457 | ||
|
|
3ef35105c8 | ||
|
|
ec768a2e89 | ||
|
|
b580d3c25a | ||
|
|
ce14f190fb | ||
|
|
4e3da005d4 | ||
|
|
0d9967e8d8 |
29
.github/workflows/build.yml
vendored
29
.github/workflows/build.yml
vendored
@@ -21,12 +21,17 @@ 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 will still be built and signed by hand
|
||||
build-win: # RCs and releases may 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
|
||||
@@ -65,6 +70,18 @@ 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: |
|
||||
@@ -142,6 +159,16 @@ 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,6 +11,11 @@ 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
|
||||
@@ -26,11 +31,79 @@ 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
|
||||
@@ -74,6 +147,14 @@ 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:
|
||||
|
||||
@@ -196,25 +196,11 @@ 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:
|
||||
@@ -254,7 +240,6 @@ 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.
|
||||
@@ -356,7 +341,6 @@ class CommonContext:
|
||||
|
||||
self.item_names = self.NameLookupDict(self, "item")
|
||||
self.location_names = self.NameLookupDict(self, "location")
|
||||
self.versions = {}
|
||||
self.checksums = {}
|
||||
|
||||
self.jsontotextparser = JSONtoTextParser(self)
|
||||
@@ -571,7 +555,6 @@ 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."""
|
||||
@@ -580,33 +563,26 @@ class CommonContext:
|
||||
|
||||
needed_updates: typing.Set[str] = set()
|
||||
for game in relevant_games:
|
||||
if game not in remote_date_package_versions and game not in remote_data_package_checksums:
|
||||
if 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 remote_version == 0 and not remote_checksum: # custom data package and no checksum for this game
|
||||
if 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 (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)
|
||||
if remote_checksum != cached_checksum:
|
||||
local_checksum: typing.Optional[str] = network_data_package["games"].get(game, {}).get("checksum")
|
||||
if ((remote_checksum or remote_version <= local_version and remote_version != 0)
|
||||
and remote_checksum == local_checksum):
|
||||
if 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 (not remote_checksum and (remote_version > cache_version or remote_version == 0)) \
|
||||
or remote_checksum != cache_checksum:
|
||||
if remote_checksum != cache_checksum:
|
||||
needed_updates.add(game)
|
||||
else:
|
||||
self.update_game(cached_game, game)
|
||||
@@ -616,7 +592,6 @@ 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):
|
||||
@@ -887,9 +862,8 @@ 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_versions, data_package_checksums)
|
||||
await ctx.prepare_data_package(set(args["games"]), data_package_checksums)
|
||||
|
||||
await ctx.server_auth(args['password'])
|
||||
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
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()
|
||||
15
Generate.py
15
Generate.py
@@ -252,7 +252,20 @@ def read_weights_yamls(path) -> Tuple[Any, ...]:
|
||||
except Exception as e:
|
||||
raise Exception(f"Failed to read weights ({path})") from e
|
||||
|
||||
return tuple(parse_yamls(yaml))
|
||||
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
|
||||
|
||||
|
||||
def interpret_on_off(value) -> bool:
|
||||
|
||||
@@ -52,22 +52,6 @@ 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
|
||||
@@ -530,7 +514,9 @@ class LinksAwakeningContext(CommonContext):
|
||||
|
||||
def run_gui(self) -> None:
|
||||
import webbrowser
|
||||
from kvui import GameManager, ImageButton
|
||||
from kvui import GameManager
|
||||
from kivy.metrics import dp
|
||||
from kivymd.uix.button import MDButton, MDButtonText
|
||||
|
||||
class LADXManager(GameManager):
|
||||
logging_pairs = [
|
||||
@@ -543,8 +529,10 @@ class LinksAwakeningContext(CommonContext):
|
||||
b = super().build()
|
||||
|
||||
if self.ctx.magpie_enabled:
|
||||
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'))
|
||||
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
|
||||
self.connect_layout.add_widget(button)
|
||||
|
||||
return b
|
||||
|
||||
1
Main.py
1
Main.py
@@ -301,6 +301,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
||||
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, Union[int, List[int]]]] = {}
|
||||
|
||||
|
||||
61
Options.py
61
Options.py
@@ -1292,42 +1292,47 @@ 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) -> typing.Dict[str, typing.Any]:
|
||||
def as_dict(
|
||||
self,
|
||||
*option_names: str,
|
||||
casing: typing.Literal["snake", "camel", "pascal", "kebab"] = "snake",
|
||||
toggles_as_bools: bool = False,
|
||||
) -> dict[str, typing.Any]:
|
||||
"""
|
||||
Returns a dictionary of [str, Option.value]
|
||||
|
||||
: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
|
||||
: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.
|
||||
"""
|
||||
assert option_names, "options.as_dict() was used without any option names."
|
||||
option_results = {}
|
||||
for option_name in option_names:
|
||||
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:
|
||||
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("_", "-")
|
||||
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
|
||||
return option_results
|
||||
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -7,7 +7,7 @@ schema>=0.7.7
|
||||
kivy>=2.3.1
|
||||
bsdiff4>=1.2.6
|
||||
platformdirs>=4.3.6
|
||||
certifi>=2025.1.31
|
||||
certifi>=2025.4.26
|
||||
cython>=3.0.12
|
||||
cymem>=2.0.11
|
||||
orjson>=3.10.15
|
||||
|
||||
@@ -47,17 +47,6 @@ 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.multiworld.random.choice(tuple(self.item_name_to_id.keys()))
|
||||
return self.random.choice(tuple(self.item_name_to_id.keys()))
|
||||
|
||||
@classmethod
|
||||
def create_group(cls, multiworld: "MultiWorld", new_player_id: int, players: Set[int]) -> World:
|
||||
|
||||
@@ -393,9 +393,7 @@ 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)
|
||||
if state.has('Hookshot', player)
|
||||
else state._lttp_has_key('Small Key (Swamp Palace)', player, 4))
|
||||
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_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)')
|
||||
|
||||
@@ -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)', '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)', '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)', '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)', 'Small Key (Swamp Palace)', 'Small Key (Swamp Palace)', 'Flippers', 'Hammer']],
|
||||
|
||||
["Swamp Palace - Compass Chest", False, []],
|
||||
["Swamp Palace - Compass Chest", False, [], ['Flippers']],
|
||||
|
||||
@@ -9,7 +9,6 @@ import random
|
||||
import re
|
||||
import string
|
||||
import subprocess
|
||||
|
||||
import sys
|
||||
import time
|
||||
import typing
|
||||
@@ -17,15 +16,16 @@ 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
|
||||
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
|
||||
|
||||
|
||||
def check_stdin() -> None:
|
||||
if Utils.is_windows and sys.stdin:
|
||||
if 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: Utils.Version = Utils.Version(0, 0, 0)
|
||||
mod_version: Version = 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"{Utils.format_SI_prefix(self.current_energy_link_value)}J"
|
||||
return f"{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 = Utils.format_SI_prefix(gained) + "J"
|
||||
gained_text = format_SI_prefix(gained) + "J"
|
||||
if gained:
|
||||
logger.debug(f"EnergyLink: Received {gained_text}. "
|
||||
f"{Utils.format_SI_prefix(args['value'])}J remaining.")
|
||||
f"{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 {Utils.format_SI_prefix(value)}J")
|
||||
logger.debug(f"EnergyLink: Sent {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 = Utils.Version(*(int(number) for number in parts[-2].split(".")))
|
||||
ctx.mod_version = Version(*(int(number) for number in parts[-2].split(".")))
|
||||
elif "Write data path: " in msg:
|
||||
ctx.write_data_path = Utils.get_text_between(msg, "Write data path: ", " [")
|
||||
ctx.write_data_path = 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,10 +521,16 @@ 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")
|
||||
options = Utils.get_settings()
|
||||
executable = options["factorio_options"]["executable"]
|
||||
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
|
||||
|
||||
server_settings = args.server_settings if args.server_settings \
|
||||
else options["factorio_options"].get("server_settings", None)
|
||||
else getattr(settings, "server_settings", None)
|
||||
server_args = ("--rcon-port", rcon_port, "--rcon-password", rcon_password)
|
||||
|
||||
|
||||
@@ -535,12 +541,8 @@ def launch():
|
||||
|
||||
if server_settings:
|
||||
server_settings = os.path.abspath(server_settings)
|
||||
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"])
|
||||
initial_filter_item_sends = bool(settings.filter_item_sends)
|
||||
initial_bridge_chat_out = bool(settings.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,7 +5,6 @@ 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
|
||||
@@ -20,6 +19,7 @@ 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,30 +27,7 @@ def launch_client():
|
||||
launch_component(launch, name="FactorioClient")
|
||||
|
||||
|
||||
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
|
||||
components.append(Component("Factorio Client", func=launch_client, component_type=Type.CLIENT))
|
||||
|
||||
|
||||
class FactorioWeb(WebWorld):
|
||||
@@ -115,6 +92,7 @@ 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)
|
||||
@@ -133,6 +111,8 @@ 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
|
||||
@@ -201,9 +181,6 @@ 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,
|
||||
@@ -218,7 +195,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 = want_progressives[progressive_item_name]
|
||||
want_progressive = self.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)
|
||||
@@ -233,6 +210,12 @@ 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)
|
||||
|
||||
26
worlds/factorio/settings.py
Normal file
26
worlds/factorio/settings.py
Normal file
@@ -0,0 +1,26 @@
|
||||
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,9 +20,11 @@ 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. 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).
|
||||
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).
|
||||
## 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.
|
||||
|
||||
@@ -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, get_base_rom_bytes, get_base_rom_path, RedDeltaPatch, BlueDeltaPatch
|
||||
from .rom import generate_output, PokemonRedProcedurePatch, PokemonBlueProcedurePatch
|
||||
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 = [RedDeltaPatch.hash]
|
||||
md5s = [PokemonRedProcedurePatch.hash]
|
||||
|
||||
class BlueRomFile(settings.UserFilePath):
|
||||
description = "Pokemon Blue (UE) ROM File"
|
||||
copy_to = "Pokemon Blue (UE) [S][!].gb"
|
||||
md5s = [BlueDeltaPatch.hash]
|
||||
md5s = [PokemonBlueProcedurePatch.hash]
|
||||
|
||||
red_rom_file: RedRomFile = RedRomFile(RedRomFile.copy_to)
|
||||
blue_rom_file: BlueRomFile = BlueRomFile(BlueRomFile.copy_to)
|
||||
@@ -113,16 +113,6 @@ 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):
|
||||
|
||||
|
||||
@@ -1,9 +1,17 @@
|
||||
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, random, data):
|
||||
|
||||
def set_mon_palettes(world: "PokemonRedBlueWorld", patch: "PokemonRedProcedurePatch | PokemonBlueProcedurePatch"):
|
||||
if world.options.randomize_pokemon_palettes == "vanilla":
|
||||
return
|
||||
pallet_map = {
|
||||
@@ -31,12 +39,9 @@ def set_mon_palettes(world, random, data):
|
||||
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 = random.choice(list(pallet_map.values()))
|
||||
pallet = world.random.choice(list(pallet_map.values()))
|
||||
palettes.append(pallet)
|
||||
address = rom_addresses["Mon_Palettes"]
|
||||
for pallet in palettes:
|
||||
data[address] = pallet
|
||||
address += 1
|
||||
patch.write_token(APTokenTypes.WRITE, rom_addresses["Mon_Palettes"], bytes(palettes))
|
||||
|
||||
|
||||
def choose_forced_type(chances, random):
|
||||
@@ -253,9 +258,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, 255)
|
||||
mon_data["catch rate"] = self.random.randint(self.options.minimum_catch_rate.value, 255)
|
||||
else:
|
||||
mon_data["catch rate"] = max(self.options.minimum_catch_rate, mon_data["catch rate"])
|
||||
mon_data["catch rate"] = max(self.options.minimum_catch_rate.value, 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"]]:
|
||||
|
||||
@@ -1,5 +1,55 @@
|
||||
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],
|
||||
@@ -7,29 +57,12 @@ disallowed2F = [[16, 2], [17, 2], [18, 2], [15, 5], [15, 6], [10, 10], [11, 10],
|
||||
[11, 1]]
|
||||
|
||||
|
||||
def randomize_rock_tunnel(data, random):
|
||||
|
||||
def randomize_rock_tunnel(patch: "PokemonRedProcedurePatch | PokemonBlueProcedurePatch", random: random.Random):
|
||||
seed = random.randint(0, 999999999999999999)
|
||||
random.seed(seed)
|
||||
|
||||
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)
|
||||
map1f = [row.copy() for row in layout1F]
|
||||
map2f = [row.copy() for row in layout2F]
|
||||
|
||||
current_map = map1f
|
||||
|
||||
@@ -305,14 +338,6 @@ def randomize_rock_tunnel(data, random):
|
||||
current_map = map2f
|
||||
check_addable_block(map2f, disallowed2F)
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
@@ -1,21 +1,66 @@
|
||||
import os
|
||||
import hashlib
|
||||
import Utils
|
||||
import bsdiff4
|
||||
import pkgutil
|
||||
from worlds.Files import APDeltaPatch
|
||||
from .text import encode_text
|
||||
import typing
|
||||
|
||||
import Utils
|
||||
from worlds.Files import APProcedurePatch, APTokenMixin, APTokenTypes
|
||||
|
||||
from . import poke_data
|
||||
from .items import item_table
|
||||
from .text import encode_text
|
||||
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
|
||||
from .regions import PokemonRBWarp, map_ids, town_map_coords
|
||||
from . import poke_data
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from . import PokemonRedBlueWorld
|
||||
|
||||
|
||||
def write_quizzes(world, data, random):
|
||||
class PokemonRedProcedurePatch(APProcedurePatch, APTokenMixin):
|
||||
game = "Pokemon Red and Blue"
|
||||
hash = "3d45c1ee9abd5738df46d2bdda8b57dc"
|
||||
patch_file_ending = ".apred"
|
||||
result_file_ending = ".gb"
|
||||
|
||||
def get_quiz(q, a):
|
||||
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):
|
||||
if q == 0:
|
||||
r = random.randint(0, 3)
|
||||
if r == 0:
|
||||
@@ -122,13 +167,13 @@ def write_quizzes(world, data, random):
|
||||
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(
|
||||
@@ -209,7 +254,7 @@ def write_quizzes(world, data, random):
|
||||
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>")
|
||||
@@ -224,46 +269,49 @@ def write_quizzes(world, data, random):
|
||||
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 = []
|
||||
question_texts: list[bytearray] = []
|
||||
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"]):
|
||||
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}"])
|
||||
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]))
|
||||
|
||||
|
||||
def generate_output(world, output_directory: str):
|
||||
random = world.random
|
||||
def generate_output(world: "PokemonRedBlueWorld", output_directory: str):
|
||||
game_version = world.options.game_version.current_key
|
||||
data = bytes(get_base_rom_bytes(game_version))
|
||||
|
||||
base_patch = pkgutil.get_data(__name__, f'basepatch_{game_version}.bsdiff4')
|
||||
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"))
|
||||
|
||||
data = bytearray(bsdiff4.patch(data, base_patch))
|
||||
def write_bytes(address: int, data: typing.Sequence[int] | int):
|
||||
if isinstance(data, int):
|
||||
data = bytes([data])
|
||||
else:
|
||||
data = bytes(data)
|
||||
|
||||
basemd5 = hashlib.md5()
|
||||
basemd5.update(data)
|
||||
patch.write_token(APTokenTypes.WRITE, address, 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 = ((0x00, 4, 0x80, 5, 0x40, 1, 0xE0, 1, 0xFF), (0x40, 2, 0x20, 5, 0x80, 5, 0xFF))
|
||||
paths = (bytes([0x00, 4, 0x80, 5, 0x40, 1, 0xE0, 1, 0xFF]), bytes([0x40, 2, 0x20, 5, 0x80, 5, 0xFF]))
|
||||
elif pallet_connections["Rival's House"] == "Oak's Lab":
|
||||
paths = ((0x00, 4, 0xC0, 3, 0x40, 1, 0xE0, 1, 0xFF), (0x40, 2, 0x10, 3, 0x80, 5, 0xFF))
|
||||
paths = (bytes([0x00, 4, 0xC0, 3, 0x40, 1, 0xE0, 1, 0xFF]), bytes([0x40, 2, 0x10, 3, 0x80, 5, 0xFF]))
|
||||
|
||||
if paths:
|
||||
write_bytes(data, paths[0], rom_addresses["Path_Pallet_Oak"])
|
||||
write_bytes(data, paths[1], rom_addresses["Path_Pallet_Player"])
|
||||
write_bytes(rom_addresses["Path_Pallet_Oak"], paths[0])
|
||||
write_bytes(rom_addresses["Path_Pallet_Player"], paths[1])
|
||||
|
||||
if pallet_connections["Rival's House"] == "Player's House 1F":
|
||||
write_bytes(data, [0x2F, 0xC7, 0x06, 0x0D, 0x00, 0x01], rom_addresses["Pallet_Fly_Coords"])
|
||||
write_bytes(rom_addresses["Pallet_Fly_Coords"], [0x2F, 0xC7, 0x06, 0x0D, 0x00, 0x01])
|
||||
elif pallet_connections["Oak's Lab"] == "Player's House 1F":
|
||||
write_bytes(data, [0x5F, 0xC7, 0x0C, 0x0C, 0x00, 0x00], rom_addresses["Pallet_Fly_Coords"])
|
||||
write_bytes(rom_addresses["Pallet_Fly_Coords"], [0x5F, 0xC7, 0x0C, 0x0C, 0x00, 0x00])
|
||||
|
||||
for region in world.multiworld.get_regions(world.player):
|
||||
for entrance in region.exits:
|
||||
@@ -281,16 +329,18 @@ def generate_output(world, output_directory: str):
|
||||
while i > len(warp_to_ids) - 1:
|
||||
i -= len(warp_to_ids)
|
||||
connected_map_name = entrance.connected_region.name.split("-")[0]
|
||||
data[address] = 0 if "Elevator" in connected_map_name else warp_to_ids[i]
|
||||
data[address + 1] = map_ids[connected_map_name]
|
||||
write_bytes(address, [
|
||||
0 if "Elevator" in connected_map_name else warp_to_ids[i],
|
||||
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:
|
||||
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]
|
||||
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])
|
||||
|
||||
if not world.options.key_items_only:
|
||||
for i, gym_leader in enumerate(("Pewter Gym - Brock TM", "Cerulean Gym - Misty TM",
|
||||
@@ -302,13 +352,13 @@ def generate_output(world, output_directory: str):
|
||||
try:
|
||||
tm = int(item_name[2:4])
|
||||
move = poke_data.moves[world.local_tms[tm - 1]]["id"]
|
||||
data[rom_addresses["Gym_Leader_Moves"] + (2 * i)] = move
|
||||
write_bytes(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
|
||||
data[rom_addresses[address]] = poke_data.pokemon_data[mon]["id"]
|
||||
write_bytes(rom_addresses[address], poke_data.pokemon_data[mon]["id"])
|
||||
world.trade_mons[address] = mon
|
||||
|
||||
if game_version == "red":
|
||||
@@ -325,141 +375,139 @@ def generate_output(world, output_directory: str):
|
||||
set_trade_mon("Trade_Doris", "Cerulean Cave 1F - Wild Pokemon - 9")
|
||||
set_trade_mon("Trade_Crinkles", "Route 12 - Wild Pokemon - 4")
|
||||
|
||||
data[rom_addresses['Fly_Location']] = world.fly_map_code
|
||||
data[rom_addresses['Map_Fly_Location']] = world.town_map_fly_map_code
|
||||
write_bytes(rom_addresses["Fly_Location"], world.fly_map_code)
|
||||
write_bytes(rom_addresses["Map_Fly_Location"], world.town_map_fly_map_code)
|
||||
|
||||
if world.options.fix_combat_bugs:
|
||||
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
|
||||
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)
|
||||
|
||||
if world.options.poke_doll_skip == "in_logic":
|
||||
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
|
||||
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
|
||||
|
||||
if world.options.bicycle_gate_skips == "patched":
|
||||
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
|
||||
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
|
||||
|
||||
if world.options.door_shuffle:
|
||||
data[rom_addresses["Entrance_Shuffle_Fuji_Warp"]] = 1 # prevent warping to Fuji's House from Pokemon Tower 7F
|
||||
write_bytes(rom_addresses["Entrance_Shuffle_Fuji_Warp"], 1) # prevent warping to Fuji's House from Pokemon Tower 7F
|
||||
|
||||
if world.options.all_elevators_locked:
|
||||
data[rom_addresses["Option_Locked_Elevator_Celadon"]] = 0x20 # jr nz
|
||||
data[rom_addresses["Option_Locked_Elevator_Silph"]] = 0x20 # jr nz
|
||||
write_bytes(rom_addresses["Option_Locked_Elevator_Celadon"], 0x20) # jr nz
|
||||
write_bytes(rom_addresses["Option_Locked_Elevator_Silph"], 0x20) # jr nz
|
||||
|
||||
if world.options.tea:
|
||||
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["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["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["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["Fossils_Needed_For_Second_Item"]] = (
|
||||
world.options.second_fossil_check_condition.value)
|
||||
write_bytes(rom_addresses["Fossils_Needed_For_Second_Item"], world.options.second_fossil_check_condition.value)
|
||||
|
||||
data[rom_addresses["Option_Lose_Money"]] = int(not world.options.lose_money_on_blackout.value)
|
||||
write_bytes(rom_addresses["Option_Lose_Money"], int(not world.options.lose_money_on_blackout.value))
|
||||
|
||||
if world.options.extra_key_items:
|
||||
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_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_Viridian_Gym_Badges']] = world.options.viridian_gym_condition.value
|
||||
data[rom_addresses['Option_EXP_Modifier']] = world.options.exp_modifier.value
|
||||
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)
|
||||
if not world.options.require_item_finder:
|
||||
data[rom_addresses['Option_Itemfinder']] = 0 # nop
|
||||
write_bytes(rom_addresses["Option_Itemfinder"], 0) # nop
|
||||
if world.options.extra_strength_boulders:
|
||||
for i in range(0, 3):
|
||||
data[rom_addresses['Option_Boulders'] + (i * 3)] = 0x15
|
||||
write_bytes(rom_addresses["Option_Boulders"] + (i * 3), 0x15)
|
||||
if world.options.extra_key_items:
|
||||
for i in range(0, 4):
|
||||
data[rom_addresses['Option_Rock_Tunnel_Extra_Items'] + (i * 3)] = 0x15
|
||||
write_bytes(rom_addresses["Option_Rock_Tunnel_Extra_Items"] + (i * 3), 0x15)
|
||||
if world.options.old_man == "open_viridian_city":
|
||||
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
|
||||
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)
|
||||
if world.options.route_3_condition == "open":
|
||||
data[rom_addresses['Option_Route3_Guard_A']] = 0x11
|
||||
write_bytes(rom_addresses["Option_Route3_Guard_A"], 0x11)
|
||||
if not world.options.robbed_house_officer:
|
||||
data[rom_addresses['Option_Trashed_House_Guard_A']] = 0x15
|
||||
data[rom_addresses['Option_Trashed_House_Guard_B']] = 0x11
|
||||
write_bytes(rom_addresses["Option_Trashed_House_Guard_A"], 0x15)
|
||||
write_bytes(rom_addresses["Option_Trashed_House_Guard_B"], 0x11)
|
||||
if world.options.require_pokedex:
|
||||
data[rom_addresses["Require_Pokedex_A"]] = 1
|
||||
data[rom_addresses["Require_Pokedex_B"]] = 1
|
||||
data[rom_addresses["Require_Pokedex_C"]] = 1
|
||||
write_bytes(rom_addresses["Require_Pokedex_A"], 1)
|
||||
write_bytes(rom_addresses["Require_Pokedex_B"], 1)
|
||||
write_bytes(rom_addresses["Require_Pokedex_C"], 1)
|
||||
else:
|
||||
data[rom_addresses["Require_Pokedex_D"]] = 0x18 # jr
|
||||
write_bytes(rom_addresses["Require_Pokedex_D"], 0x18) # jr
|
||||
if world.options.dexsanity:
|
||||
data[rom_addresses["Option_Dexsanity_A"]] = 1
|
||||
data[rom_addresses["Option_Dexsanity_B"]] = 1
|
||||
write_bytes(rom_addresses["Option_Dexsanity_A"], 1)
|
||||
write_bytes(rom_addresses["Option_Dexsanity_B"], 1)
|
||||
if world.options.all_pokemon_seen:
|
||||
data[rom_addresses["Option_Pokedex_Seen"]] = 1
|
||||
write_bytes(rom_addresses["Option_Pokedex_Seen"], 1)
|
||||
money = str(world.options.starting_money.value).zfill(6)
|
||||
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"])
|
||||
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:])))
|
||||
|
||||
if world.options.badges_needed_for_hm_moves.value == 0:
|
||||
for hm_move in poke_data.hm_moves:
|
||||
write_bytes(data, bytearray([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]),
|
||||
rom_addresses["HM_" + hm_move + "_Badge_a"])
|
||||
write_bytes(rom_addresses["HM_" + hm_move + "_Badge_a"], [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])
|
||||
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():
|
||||
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]
|
||||
write_bytes(rom_addresses["HM_" + hm_move + "_Badge_b"], badge_codes[badge])
|
||||
move_text = hm_move
|
||||
if badge not in ["Marsh Badge", "Volcano Badge", "Earth Badge"]:
|
||||
move_text = ", " + move_text
|
||||
@@ -467,62 +515,58 @@ def generate_output(world, output_directory: str):
|
||||
if badge in written_badges:
|
||||
rom_address += len(written_badges[badge])
|
||||
move_text = ", " + move_text
|
||||
write_bytes(data, encode_text(move_text.upper()), rom_address)
|
||||
write_bytes(rom_address, encode_text(move_text.upper()))
|
||||
written_badges[badge] = move_text
|
||||
for badge in ["Marsh Badge", "Volcano Badge", "Earth Badge"]:
|
||||
if badge not in written_badges:
|
||||
write_bytes(data, encode_text("Nothing"), rom_addresses["Badge_Text_" + badge.replace(" ", "_")])
|
||||
write_bytes(rom_addresses["Badge_Text_" + badge.replace(" ", "_")], encode_text("Nothing"))
|
||||
|
||||
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
|
||||
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]
|
||||
write_bytes(type_loc, [poke_data.type_ids[matchup[0]], poke_data.type_ids[matchup[1]], matchup[2]])
|
||||
type_loc += 3
|
||||
data[type_loc] = 0xFF
|
||||
data[type_loc + 1] = 0xFF
|
||||
data[type_loc + 2] = 0xFF
|
||||
write_bytes(type_loc, b"\xFF\xFF\xFF")
|
||||
|
||||
if world.options.normalize_encounter_chances.value:
|
||||
chances = [25, 51, 77, 103, 129, 155, 180, 205, 230, 255]
|
||||
for i, chance in enumerate(chances):
|
||||
data[rom_addresses['Encounter_Chances'] + (i * 2)] = chance
|
||||
write_bytes(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))
|
||||
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)
|
||||
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"])
|
||||
if mon in world.learnsets and world.learnsets[mon]:
|
||||
address = rom_addresses["Learnset_" + mon.replace(" ", "")]
|
||||
for i, move in enumerate(world.learnsets[mon]):
|
||||
data[(address + 1) + i * 2] = poke_data.moves[move]["id"]
|
||||
write_bytes((address + 1) + i * 2, poke_data.moves[move]["id"])
|
||||
|
||||
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
|
||||
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)
|
||||
|
||||
if world.options.safari_zone_normal_battles.value == 1:
|
||||
data[rom_addresses["Option_Safari_Zone_Battle_Type"]] = 255
|
||||
write_bytes(rom_addresses["Option_Safari_Zone_Battle_Type"], 255)
|
||||
|
||||
if world.options.reusable_tms.value:
|
||||
data[rom_addresses["Option_Reusable_TMs"]] = 0xC9
|
||||
write_bytes(rom_addresses["Option_Reusable_TMs"], 0xC9)
|
||||
|
||||
data[rom_addresses["Option_Always_Half_STAB"]] = int(not world.options.same_type_attack_bonus.value)
|
||||
write_bytes(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"]
|
||||
@@ -531,43 +575,45 @@ def generate_output(world, 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 = bytearray([0xFE, len(inventory)])
|
||||
shop_data += bytearray([item_table[item].id - 172000000 for item in inventory])
|
||||
shop_data = [0xFE, len(inventory)]
|
||||
shop_data += [item_table[item].id - 172000000 for item in inventory]
|
||||
shop_data.append(0xFF)
|
||||
for shop in range(1, 11):
|
||||
write_bytes(data, shop_data, rom_addresses[f"Shop{shop}"])
|
||||
write_bytes(rom_addresses[f"Shop{shop}"], shop_data)
|
||||
if world.options.stonesanity:
|
||||
write_bytes(data, bytearray([0xFE, 1, item_table["Poke Doll"].id - 172000000, 0xFF]), rom_addresses[f"Shop_Stones"])
|
||||
write_bytes(rom_addresses["Shop_Stones"], [0xFE, 1, item_table["Poke Doll"].id - 172000000, 0xFF])
|
||||
|
||||
price = str(world.options.master_ball_price.value).zfill(6)
|
||||
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
|
||||
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
|
||||
|
||||
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
|
||||
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))
|
||||
|
||||
set_mon_palettes(world, random, data)
|
||||
set_mon_palettes(world, patch)
|
||||
|
||||
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(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)
|
||||
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"]])
|
||||
|
||||
TM_IDs = bytearray([poke_data.moves[move]["id"] for move in world.local_tms])
|
||||
write_bytes(data, TM_IDs, rom_addresses["TM_Moves"])
|
||||
TM_IDs = [poke_data.moves[move]["id"] for move in world.local_tms]
|
||||
write_bytes(rom_addresses["TM_Moves"], TM_IDs)
|
||||
|
||||
if world.options.randomize_rock_tunnel:
|
||||
seed = randomize_rock_tunnel(data, random)
|
||||
write_bytes(data, encode_text(f"SEED: <LINE>{seed}"), rom_addresses["Text_Rock_Tunnel_Sign"])
|
||||
seed = randomize_rock_tunnel(patch, world.random)
|
||||
write_bytes(rom_addresses["Text_Rock_Tunnel_Sign"], encode_text(f"SEED: <LINE>{seed}"))
|
||||
|
||||
mons = [mon["id"] for mon in poke_data.pokemon_data.values()]
|
||||
random.shuffle(mons)
|
||||
data[rom_addresses['Title_Mon_First']] = mons.pop()
|
||||
world.random.shuffle(mons)
|
||||
write_bytes(rom_addresses["Title_Mon_First"], mons.pop())
|
||||
for mon in range(0, 16):
|
||||
data[rom_addresses['Title_Mons'] + mon] = mons.pop()
|
||||
write_bytes(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
|
||||
@@ -576,34 +622,34 @@ def generate_output(world, 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(data, encode_text(world.multiworld.seed_name[-20:], 20, True), rom_addresses['Title_Seed'])
|
||||
write_bytes(rom_addresses["Title_Seed"], encode_text(world.multiworld.seed_name[-20:], 20, True))
|
||||
|
||||
slot_name = world.multiworld.player_name[world.player]
|
||||
slot_name.replace("@", " ")
|
||||
slot_name.replace("<", " ")
|
||||
slot_name.replace(">", " ")
|
||||
write_bytes(data, encode_text(slot_name, 16, True, True), rom_addresses['Title_Slot_Name'])
|
||||
write_bytes(rom_addresses["Title_Slot_Name"], encode_text(slot_name, 16, True, True))
|
||||
|
||||
if world.trainer_name == "choose_in_game":
|
||||
data[rom_addresses["Skip_Player_Name"]] = 0
|
||||
write_bytes(rom_addresses["Skip_Player_Name"], 0)
|
||||
else:
|
||||
write_bytes(data, world.trainer_name, rom_addresses['Player_Name'])
|
||||
write_bytes(rom_addresses["Player_Name"], world.trainer_name)
|
||||
if world.rival_name == "choose_in_game":
|
||||
data[rom_addresses["Skip_Rival_Name"]] = 0
|
||||
write_bytes(rom_addresses["Skip_Rival_Name"], 0)
|
||||
else:
|
||||
write_bytes(data, world.rival_name, rom_addresses['Rival_Name'])
|
||||
write_bytes(rom_addresses["Rival_Name"], world.rival_name)
|
||||
|
||||
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]
|
||||
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]
|
||||
rom_name.extend([0] * (21 - len(rom_name)))
|
||||
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)
|
||||
write_bytes(0xFFC6, rom_name)
|
||||
write_bytes(0xFFDB, world.multiworld.seed_name.encode())
|
||||
write_bytes(0xFFF0, world.multiworld.player_name[world.player].encode())
|
||||
|
||||
world.finished_level_scaling.wait()
|
||||
|
||||
write_quizzes(world, data, random)
|
||||
write_quizzes(world, patch)
|
||||
|
||||
for location in world.multiworld.get_locations(world.player):
|
||||
if location.party_data:
|
||||
@@ -617,18 +663,18 @@ def generate_output(world, output_directory: str):
|
||||
levels = party["level"]
|
||||
for address, party in zip(addresses, parties):
|
||||
if isinstance(levels, int):
|
||||
data[address] = levels
|
||||
write_bytes(address, levels)
|
||||
address += 1
|
||||
for mon in party:
|
||||
data[address] = poke_data.pokemon_data[mon]["id"]
|
||||
write_bytes(address, poke_data.pokemon_data[mon]["id"])
|
||||
address += 1
|
||||
else:
|
||||
address += 1
|
||||
for level, mon in zip(levels, party):
|
||||
data[address] = level
|
||||
data[address + 1] = poke_data.pokemon_data[mon]["id"]
|
||||
write_bytes(address, [level, poke_data.pokemon_data[mon]["id"]])
|
||||
address += 2
|
||||
assert data[address] == 0 or location.name == "Fossil Level - Trainer Parties"
|
||||
# This assert can't be done with procedure patch tokens.
|
||||
# assert data[address] == 0 or location.name == "Fossil Level - Trainer Parties"
|
||||
continue
|
||||
elif location.rom_address is None:
|
||||
continue
|
||||
@@ -639,85 +685,24 @@ def generate_output(world, output_directory: str):
|
||||
rom_address = [rom_address]
|
||||
for address in rom_address:
|
||||
if location.item.name in poke_data.pokemon_data.keys():
|
||||
data[address] = poke_data.pokemon_data[location.item.name]["id"]
|
||||
write_bytes(address, poke_data.pokemon_data[location.item.name]["id"])
|
||||
elif " ".join(location.item.name.split()[1:]) in poke_data.pokemon_data.keys():
|
||||
data[address] = poke_data.pokemon_data[" ".join(location.item.name.split()[1:])]["id"]
|
||||
write_bytes(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
|
||||
data[address] = item_id
|
||||
write_bytes(address, item_id)
|
||||
if location.level:
|
||||
data[location.level_address] = location.level
|
||||
write_bytes(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:
|
||||
data[address] = 0x2C # AP Item
|
||||
write_bytes(address, 0x2C) # AP Item
|
||||
|
||||
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)
|
||||
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}"))
|
||||
|
||||
@@ -89,9 +89,12 @@ class ROM(object):
|
||||
|
||||
class FakeROM(ROM):
|
||||
# to have the same code for real ROM and the webservice
|
||||
def __init__(self, data={}):
|
||||
def __init__(self, data=None):
|
||||
super(FakeROM, self).__init__()
|
||||
self.data = data
|
||||
if data is None:
|
||||
self.data = {}
|
||||
else:
|
||||
self.data = data
|
||||
self.ipsPatches = []
|
||||
|
||||
def write(self, bytes):
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
# 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,9 +1,11 @@
|
||||
import logging
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
from NetUtils import ClientStatus, color
|
||||
from NetUtils import ClientStatus, NetworkItem, color
|
||||
from worlds.AutoSNIClient import SNIClient
|
||||
from .Names.TextBox import generate_received_text
|
||||
from .Names.TextBox import generate_received_text, generate_received_trap_link_text
|
||||
from .Items import trap_value_to_name, trap_name_to_value
|
||||
|
||||
snes_logger = logging.getLogger("SNES")
|
||||
|
||||
@@ -42,10 +44,13 @@ 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
|
||||
@@ -76,6 +81,7 @@ 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
|
||||
@@ -111,6 +117,84 @@ 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
|
||||
|
||||
@@ -123,9 +207,11 @@ 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
|
||||
|
||||
@@ -133,6 +219,15 @@ 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
|
||||
|
||||
@@ -142,12 +237,17 @@ 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
|
||||
@@ -206,7 +306,8 @@ 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:
|
||||
if (not hasattr(self, "trap_queue") or len(self.trap_queue) == 0) and\
|
||||
(not hasattr(self, "priority_trap") or self.priority_trap == 0):
|
||||
return
|
||||
|
||||
game_state = await snes_read(ctx, SMW_GAME_STATE_ADDR, 0x1)
|
||||
@@ -221,7 +322,24 @@ class SMWSNIClient(SNIClient):
|
||||
if pause_state[0] != 0x00:
|
||||
return
|
||||
|
||||
next_trap, message = self.trap_queue.pop(0)
|
||||
|
||||
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
|
||||
|
||||
from .Rom import trap_rom_data
|
||||
if next_trap.item in trap_rom_data:
|
||||
@@ -231,16 +349,22 @@ 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
|
||||
self.add_trap_to_queue(next_trap, message)
|
||||
if from_queue:
|
||||
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
|
||||
self.add_trap_to_queue(next_trap, message)
|
||||
if from_queue:
|
||||
self.add_trap_to_queue(next_trap, message)
|
||||
return
|
||||
else:
|
||||
if next_trap.item == 0xBC001D:
|
||||
@@ -248,12 +372,18 @@ 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:
|
||||
self.add_trap_to_queue(next_trap, message)
|
||||
if from_queue:
|
||||
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]))
|
||||
|
||||
@@ -270,9 +400,75 @@ 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
|
||||
|
||||
@@ -333,6 +529,7 @@ 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)
|
||||
@@ -506,7 +703,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 not in trap_rom_data:
|
||||
if item.item != 0xBC0012 and item.item != 0xBC0015 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]
|
||||
@@ -515,7 +712,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:
|
||||
if item.item in trap_rom_data or item.item == 0xBC0015:
|
||||
item_name = ctx.item_names.lookup_in_game(item.item)
|
||||
player_name = ctx.player_names[item.player]
|
||||
|
||||
@@ -572,14 +769,6 @@ 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,3 +75,49 @@ 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,6 +117,31 @@ 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,6 +398,20 @@ 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,
|
||||
@@ -447,6 +461,8 @@ 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, 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, 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, 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,6 +3160,8 @@ 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,6 +90,7 @@ class SMWWorld(World):
|
||||
"blocksanity",
|
||||
)
|
||||
slot_data["active_levels"] = self.active_level_dict
|
||||
slot_data["trap_weights"] = self.output_trap_weights()
|
||||
|
||||
return slot_data
|
||||
|
||||
@@ -322,3 +323,15 @@ 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
|
||||
|
||||
@@ -299,17 +299,9 @@ class StardewValleyWorld(World):
|
||||
|
||||
return StardewItem(item.name, override_classification, item.code, self.player)
|
||||
|
||||
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
|
||||
|
||||
def create_event_location(self, location_data: LocationData, rule: StardewRule, item: str):
|
||||
region = self.multiworld.get_region(location_data.region, self.player)
|
||||
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))
|
||||
region.add_event(location_data.name, item, rule, StardewLocation, StardewItem)
|
||||
|
||||
def set_rules(self):
|
||||
set_rules(self)
|
||||
|
||||
@@ -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.skill.can_fish(difficulty=50)
|
||||
fish_rule = self.logic.fishing.can_fish_anywhere(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.skill.can_fish(difficulty=60)
|
||||
return self.logic.fishing.can_fish(60)
|
||||
|
||||
def has_all_rarecrows(self) -> StardewRule:
|
||||
rules = []
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from functools import cached_property
|
||||
|
||||
from Utils import cache_self1
|
||||
from .base_logic import BaseLogicMixin, BaseLogic
|
||||
from ..data import fish_data
|
||||
@@ -12,6 +14,8 @@ 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):
|
||||
@@ -20,17 +24,35 @@ class FishingLogicMixin(BaseLogicMixin):
|
||||
|
||||
|
||||
class FishingLogic(BaseLogic):
|
||||
def can_fish_in_freshwater(self) -> StardewRule:
|
||||
return self.logic.skill.can_fish() & self.logic.region.can_reach_any((Region.forest, Region.town, Region.mountain))
|
||||
@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))
|
||||
|
||||
@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.skill.can_fish() & self.logic.region.can_reach(region)
|
||||
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
|
||||
|
||||
@cache_self1
|
||||
def can_catch_fish(self, fish: FishItem) -> StardewRule:
|
||||
@@ -39,14 +61,17 @@ 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.skill.can_crab_pot
|
||||
difficulty_rule = self.logic.fishing.can_crab_pot
|
||||
else:
|
||||
difficulty_rule = self.logic.skill.can_fish(difficulty=(120 if fish.legendary else fish.difficulty))
|
||||
difficulty_rule = self.logic.fishing.can_fish(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:
|
||||
@@ -78,7 +103,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)
|
||||
@@ -89,3 +114,23 @@ 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.skill.can_fish() & self.building.has_building(Building.fish_pond),
|
||||
AnimalProduct.roe: self.fishing.can_fish_anywhere() & 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.skill.can_fish(Region.witch_swamp)) | (self.artisan.can_mayonnaise(AnimalProduct.void_egg)),
|
||||
ArtisanGood.void_mayonnaise: 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.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.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.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.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),
|
||||
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),
|
||||
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.skill.can_fish(Region.dig_site),
|
||||
Fossil.fossilized_spine: self.fishing.can_fish_at(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.skill.can_crab_pot,
|
||||
Trash.broken_glasses: self.skill.can_crab_pot,
|
||||
Trash.driftwood: self.skill.can_crab_pot,
|
||||
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.joja_cola: self.money.can_spend_at(Region.saloon, 75),
|
||||
Trash.soggy_newspaper: self.skill.can_crab_pot,
|
||||
Trash.trash: self.skill.can_crab_pot,
|
||||
Trash.soggy_newspaper: self.fishing.can_crab_pot_anywhere,
|
||||
Trash.trash: self.fishing.can_crab_pot_anywhere,
|
||||
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.skill.can_fish(Region.tide_pools),
|
||||
WaterItem.white_algae: self.skill.can_fish(Region.mines_floor_20),
|
||||
WaterItem.seaweed: self.fishing.can_fish_at(Region.tide_pools),
|
||||
WaterItem.white_algae: self.fishing.can_fish_at(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.skill.can_fish(),
|
||||
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.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,7 +86,9 @@ 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),
|
||||
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.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,13 +1,10 @@
|
||||
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
|
||||
@@ -15,7 +12,6 @@ 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")
|
||||
|
||||
|
||||
@@ -138,44 +134,9 @@ class SkillLogic(BaseLogic):
|
||||
@cached_property
|
||||
def can_get_fishing_xp(self) -> StardewRule:
|
||||
if self.content.features.skill_progression.is_progressive:
|
||||
return self.logic.skill.can_fish() | self.logic.skill.can_crab_pot
|
||||
return self.logic.fishing.can_fish_anywhere() | self.logic.fishing.can_crab_pot
|
||||
|
||||
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
|
||||
return self.logic.fishing.can_fish_anywhere()
|
||||
|
||||
def can_forage_quality(self, quality: str) -> StardewRule:
|
||||
if quality == ForageQuality.basic:
|
||||
|
||||
@@ -42,7 +42,7 @@ class SpecialOrderLogic(BaseLogic):
|
||||
SpecialOrder.fragments_of_the_past: self.logic.monster.can_kill(Monster.skeleton),
|
||||
SpecialOrder.gus_famous_omelet: self.logic.has(AnimalProduct.any_egg),
|
||||
SpecialOrder.crop_order: self.logic.ability.can_farm_perfectly() & self.logic.shipping.can_use_shipping_bin,
|
||||
SpecialOrder.community_cleanup: self.logic.skill.can_crab_pot,
|
||||
SpecialOrder.community_cleanup: self.logic.fishing.can_crab_pot_anywhere,
|
||||
SpecialOrder.the_strong_stuff: self.logic.has(ArtisanGood.specific_juice(Vegetable.potato)),
|
||||
SpecialOrder.pierres_prime_produce: self.logic.ability.can_farm_perfectly(),
|
||||
SpecialOrder.robins_project: self.logic.relationship.can_meet(NPC.robin) & self.logic.ability.can_chop_perfectly() &
|
||||
|
||||
@@ -44,9 +44,9 @@ class ModSkillLogic(BaseLogic):
|
||||
|
||||
def can_earn_luck_skill_level(self, level: int) -> StardewRule:
|
||||
if level >= 6:
|
||||
return self.logic.fishing.can_fish_chests() | self.logic.action.can_open_geode(Geode.magma)
|
||||
return self.logic.fishing.can_fish_chests | self.logic.action.can_open_geode(Geode.magma)
|
||||
if level >= 3:
|
||||
return self.logic.fishing.can_fish_chests() | self.logic.action.can_open_geode(Geode.geode)
|
||||
return self.logic.fishing.can_fish_chests | self.logic.action.can_open_geode(Geode.geode)
|
||||
return True_() # You can literally wake up and or get them by opening starting chests.
|
||||
|
||||
def can_earn_magic_skill_level(self, level: int) -> StardewRule:
|
||||
|
||||
@@ -214,7 +214,7 @@ def set_entrance_rules(logic: StardewLogic, multiworld, player, world_options: S
|
||||
|
||||
set_entrance_rule(multiworld, player, Entrance.mountain_to_railroad, logic.received("Railroad Boulder Removed"))
|
||||
set_entrance_rule(multiworld, player, Entrance.enter_witch_warp_cave, logic.quest.has_dark_talisman() | (logic.mod.magic.can_blink()))
|
||||
set_entrance_rule(multiworld, player, Entrance.enter_witch_hut, (logic.has(ArtisanGood.void_mayonnaise) | logic.mod.magic.can_blink()))
|
||||
set_entrance_rule(multiworld, player, Entrance.enter_witch_hut, (logic.quest.can_complete_quest(Quest.goblin_problem) | logic.mod.magic.can_blink()))
|
||||
set_entrance_rule(multiworld, player, Entrance.enter_mutant_bug_lair,
|
||||
(logic.wallet.has_rusty_key() & logic.region.can_reach(Region.railroad) & logic.relationship.can_meet(
|
||||
NPC.krobus)) | logic.mod.magic.can_blink())
|
||||
@@ -923,9 +923,9 @@ def set_magic_spell_rules(logic: StardewLogic, multiworld: MultiWorld, player: i
|
||||
set_rule(multiworld.get_location("Analyze: Fireball", player),
|
||||
logic.has("Fire Quartz"))
|
||||
set_rule(multiworld.get_location("Analyze: Frostbolt", player),
|
||||
logic.region.can_reach(Region.mines_floor_60) & logic.skill.can_fish(difficulty=85))
|
||||
logic.region.can_reach(Region.mines_floor_60) & logic.fishing.can_fish(85))
|
||||
set_rule(multiworld.get_location("Analyze All Elemental School Locations", player),
|
||||
logic.has("Fire Quartz") & logic.region.can_reach(Region.mines_floor_60) & logic.skill.can_fish(difficulty=85))
|
||||
logic.has("Fire Quartz") & logic.region.can_reach(Region.mines_floor_60) & logic.fishing.can_fish(85))
|
||||
# set_rule(multiworld.get_location("Analyze: Lantern", player),)
|
||||
set_rule(multiworld.get_location("Analyze: Tendrils", player),
|
||||
logic.region.can_reach(Region.farm))
|
||||
@@ -948,7 +948,7 @@ def set_magic_spell_rules(logic: StardewLogic, multiworld: MultiWorld, player: i
|
||||
& (logic.tool.has_tool("Axe", "Basic") | logic.tool.has_tool("Pickaxe", "Basic")) &
|
||||
logic.has("Coffee") & logic.has("Life Elixir")
|
||||
& logic.ability.can_mine_perfectly() & logic.has("Earth Crystal") &
|
||||
logic.has("Fire Quartz") & logic.skill.can_fish(difficulty=85) &
|
||||
logic.has("Fire Quartz") & logic.fishing.can_fish(85) &
|
||||
logic.region.can_reach(Region.witch_hut) &
|
||||
logic.region.can_reach(Region.mines_floor_100) &
|
||||
logic.region.can_reach(Region.farm) & logic.time.has_lived_months(12)))
|
||||
|
||||
@@ -486,10 +486,10 @@ class TunicWorld(World):
|
||||
multiworld.random.shuffle(non_grass_fill_locations)
|
||||
|
||||
for filler_item in grass_fill:
|
||||
multiworld.push_item(grass_fill_locations.pop(), filler_item, collect=False)
|
||||
grass_fill_locations.pop().place_locked_item(filler_item)
|
||||
|
||||
for filler_item in non_grass_fill:
|
||||
multiworld.push_item(non_grass_fill_locations.pop(), filler_item, collect=False)
|
||||
non_grass_fill_locations.pop().place_locked_item(filler_item)
|
||||
|
||||
def create_regions(self) -> None:
|
||||
self.tunic_portal_pairs = {}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from Options import (
|
||||
Choice,
|
||||
@@ -18,6 +19,8 @@ from .Locations import DUNGEON_NAMES
|
||||
class Dungeons(DefaultOnToggle):
|
||||
"""
|
||||
This controls whether dungeon locations are randomized.
|
||||
|
||||
This means the items found in dungeons will be randomized, not that the entrances to dungeons will be randomized.
|
||||
"""
|
||||
|
||||
display_name = "Dungeons"
|
||||
@@ -752,6 +755,68 @@ class TWWOptions(PerGameCommonOptions):
|
||||
remove_music: RemoveMusic
|
||||
death_link: DeathLink
|
||||
|
||||
def get_output_dict(self) -> dict[str, Any]:
|
||||
"""
|
||||
Returns a dictionary of option name to value to be placed in
|
||||
the output APTWW file.
|
||||
|
||||
:return: Dictionary of option name to value for the output file.
|
||||
"""
|
||||
|
||||
# Note: these options' values must be able to be passed through
|
||||
# `yaml.safe_dump`.
|
||||
return self.as_dict(
|
||||
"progression_dungeons",
|
||||
"progression_tingle_chests",
|
||||
"progression_dungeon_secrets",
|
||||
"progression_puzzle_secret_caves",
|
||||
"progression_combat_secret_caves",
|
||||
"progression_savage_labyrinth",
|
||||
"progression_great_fairies",
|
||||
"progression_short_sidequests",
|
||||
"progression_long_sidequests",
|
||||
"progression_spoils_trading",
|
||||
"progression_minigames",
|
||||
"progression_battlesquid",
|
||||
"progression_free_gifts",
|
||||
"progression_mail",
|
||||
"progression_platforms_rafts",
|
||||
"progression_submarines",
|
||||
"progression_eye_reef_chests",
|
||||
"progression_big_octos_gunboats",
|
||||
"progression_triforce_charts",
|
||||
"progression_treasure_charts",
|
||||
"progression_expensive_purchases",
|
||||
"progression_island_puzzles",
|
||||
"progression_misc",
|
||||
"randomize_mapcompass",
|
||||
"randomize_smallkeys",
|
||||
"randomize_bigkeys",
|
||||
"sword_mode",
|
||||
"required_bosses",
|
||||
"num_required_bosses",
|
||||
"chest_type_matches_contents",
|
||||
"hero_mode",
|
||||
"logic_obscurity",
|
||||
"logic_precision",
|
||||
"randomize_dungeon_entrances",
|
||||
"randomize_secret_cave_entrances",
|
||||
"randomize_miniboss_entrances",
|
||||
"randomize_boss_entrances",
|
||||
"randomize_secret_cave_inner_entrances",
|
||||
"randomize_fairy_fountain_entrances",
|
||||
"mix_entrances",
|
||||
"randomize_enemies",
|
||||
"randomize_starting_island",
|
||||
"randomize_charts",
|
||||
"swift_sail",
|
||||
"instant_text_boxes",
|
||||
"reveal_full_sea_chart",
|
||||
"add_shortcut_warps_between_dungeons",
|
||||
"skip_rematch_bosses",
|
||||
"remove_music",
|
||||
)
|
||||
|
||||
|
||||
tww_option_groups: list[OptionGroup] = [
|
||||
OptionGroup(
|
||||
|
||||
@@ -462,7 +462,7 @@ class TWWWorld(World):
|
||||
"Seed": multiworld.seed_name,
|
||||
"Slot": player,
|
||||
"Name": self.player_name,
|
||||
"Options": self.options.as_dict(*self.options_dataclass.type_hints),
|
||||
"Options": self.options.get_output_dict(),
|
||||
"Required Bosses": self.boss_reqs.required_boss_item_locations,
|
||||
"Locations": {},
|
||||
"Entrances": {},
|
||||
|
||||
@@ -19,17 +19,18 @@ a yellow Rupee, which includes a message that the location is not randomized.
|
||||
## What is the goal of The Wind Waker?
|
||||
|
||||
Reach and defeat Ganondorf atop Ganon's Tower. This will require all eight shards of the Triforce of Courage, the
|
||||
fully-powered Master Sword (unless it's swordless mode), Light Arrows, and any other items necessary to reach Ganondorf.
|
||||
fully-powered Master Sword (unless it's swords optional or swordless mode), Light Arrows, and any other items necessary
|
||||
to reach Ganondorf.
|
||||
|
||||
## What does another world's item look like in TWW?
|
||||
|
||||
Items belonging to other non-TWW worlds are represented by Father's Letter (the letter Medli gives you to give to
|
||||
Komali), an unused item in the randomizer.
|
||||
|
||||
## When the player receives an item, what happens?
|
||||
## What happens when the player receives an item?
|
||||
|
||||
When the player receives an item, it will automatically be added to Link's inventory. Unlike many other Zelda
|
||||
randomizers, Link **will not** hold the item above his head.
|
||||
When the player receives an item, it will automatically be added to Link's inventory. Link **will not** hold the item
|
||||
above his head like many other Zelda randomizers.
|
||||
|
||||
## I need help! What do I do?
|
||||
|
||||
@@ -37,16 +38,20 @@ Refer to the [FAQ](https://lagolunatic.github.io/wwrando/faq/) first. Then, try
|
||||
[setup guide](/tutorial/The%20Wind%20Waker/setup/en). If you are still stuck, please ask in the Wind Waker channel in
|
||||
the Archipelago server.
|
||||
|
||||
## I opened the game in Dolphin, but I don't have any of my starting items!
|
||||
|
||||
You must connect to the multiworld room to receive any items, including your starting inventory.
|
||||
|
||||
## Known issues
|
||||
|
||||
- Randomized freestanding rupees, spoils, and bait will also be given to the player picking up the item. The item will
|
||||
be sent properly, but the collecting player will receive an extra copy.
|
||||
- Demo items (items which are held over Link's head) which are **not** randomized, such as rupees from salvages from
|
||||
random light rings or rewards from minigames, will not work.
|
||||
- Demo items (items held over Link's head) that are **not** randomized, such as rupees from salvages from random light
|
||||
rings or rewards from minigames, will not work.
|
||||
- Item get messages for progressive items received on locations that send earlier than intended will be incorrect. This
|
||||
does not affect gameplay.
|
||||
- The Heart Piece count in item get messages will be off by one. This does not affect gameplay.
|
||||
- It has been reported that item links can be buggy. Nothing game-breaking, but do be aware of it.
|
||||
- It has been reported that item links can be buggy. It is nothing game-breaking, but do be aware of it.
|
||||
|
||||
Feel free to report any other issues or suggest improvements in the Wind Waker channel in the Archipelago server!
|
||||
|
||||
@@ -76,14 +81,14 @@ A few presets are available on the [player options page](../player-options) for
|
||||
The preset features 3 required bosses and hard obscurity difficulty, and while the list of enabled progression options
|
||||
may seem intimidating, the preset also excludes several locations.
|
||||
- **Miniblins 2025**: These are (as close to as possible) the settings used in the WWR Racing Server's
|
||||
[2025 Season of Minblins](https://docs.google.com/document/d/19vT68eU6PepD2BD2ZjR9ikElfqs8pXfqQucZ-TcscV8). This
|
||||
[2025 Season of Miniblins](https://docs.google.com/document/d/19vT68eU6PepD2BD2ZjR9ikElfqs8pXfqQucZ-TcscV8). This
|
||||
preset is great if you're new to Wind Waker! There aren't too many locations in the world, and you only need to
|
||||
complete two dungeons. You also start with many convenience items, such as double magic, a capacity upgrade for your
|
||||
bow and bombs, and six hearts.
|
||||
- **Mixed Pools**: These are the settings used in the WWR Racing Server's
|
||||
[Mixed Pools Co-op Tournament](https://docs.google.com/document/d/1YGPTtEgP978TIi0PUAD792OtZbE2jBQpI8XCAy63qpg). This
|
||||
preset features full entrance rando and includes many locations behind a randomized entrance. There are also a bunch
|
||||
of overworld locations, as these settings were intended to be played in a two-person co-op team. The preset also has 6
|
||||
preset features full entrance rando and includes most locations behind a randomized entrance. There are also many
|
||||
overworld locations, as these settings were intended to be played in a two-person co-op team. The preset also has 6
|
||||
required bosses, but since entrance pools are randomized, the bosses could be found anywhere! Check your Sea Chart to
|
||||
find out which island the bosses are on.
|
||||
|
||||
@@ -106,7 +111,7 @@ This randomizer would not be possible without the help from:
|
||||
- CrainWWR: (multiworld and Dolphin memory assistance, additional programming)
|
||||
- Cyb3R: (reference for `TWWClient`)
|
||||
- DeamonHunter: (additional programming)
|
||||
- Dev5ter: (initial TWW AP implmentation)
|
||||
- Dev5ter: (initial TWW AP implementation)
|
||||
- Gamma / SageOfMirrors: (additional programming)
|
||||
- LagoLunatic: (base randomizer, additional assistance)
|
||||
- Lunix: (Linux support, additional programming)
|
||||
|
||||
@@ -5,11 +5,13 @@ If you're playing The Wind Waker, you must follow a few simple steps to get star
|
||||
|
||||
## Requirements
|
||||
|
||||
You'll need the following components to be able to play with The Wind Waker:
|
||||
You'll need the following components to be able to play The Wind Waker:
|
||||
* Install [Dolphin Emulator](https://dolphin-emu.org/download/). **We recommend using the latest release.**
|
||||
* For Linux users, you can use the flatpak package
|
||||
* Linux users can use the flatpak package
|
||||
[available on Flathub](https://flathub.org/apps/org.DolphinEmu.dolphin-emu).
|
||||
* The 2.5.0 version of the [TWW AP Randomizer Build](https://github.com/tanjo3/wwrando/releases/tag/ap_2.5.0).
|
||||
* The latest version of the [TWW AP Randomizer Build](https://github.com/tanjo3/wwrando/releases?q=tag%3Aap_2).
|
||||
* Please note that this build is **different** from the one the standalone randomizer uses. This build is
|
||||
specifically for Archipelago.
|
||||
* A The Wind Waker ISO (North American version), probably named "Legend of Zelda, The - The Wind Waker (USA).iso".
|
||||
|
||||
Optionally, you can also download:
|
||||
@@ -26,17 +28,17 @@ world. Once you're happy with your settings, provide the room host with your YAM
|
||||
|
||||
## Connecting to a Room
|
||||
|
||||
The multiworld host will provide you a link to download your `aptww` file or a zip file containing everyone's files. The
|
||||
`aptww` file should be named `P#_<name>_XXXXX.aptww`, where `#` is your player ID, `<name>` is your player name, and
|
||||
The multiworld host will provide you a link to download your APTWW file or a zip file containing everyone's files. The
|
||||
APTWW file should be named `P#_<name>_XXXXX.aptww`, where `#` is your player ID, `<name>` is your player name, and
|
||||
`XXXXX` is the room ID. The host should also provide you with the room's server name and port number.
|
||||
|
||||
Once you do, follow these steps to connect to the room:
|
||||
Once you're ready, follow these steps to connect to the room:
|
||||
1. Run the TWW AP Randomizer Build. If this is the first time you've opened the randomizer, you'll need to specify the
|
||||
path to your The Wind Waker ISO and the output folder for the randomized ISO. These will be saved for the next time you
|
||||
open the program.
|
||||
2. Modify any cosmetic convenience tweaks and player customization options as desired.
|
||||
3. For the APTWW file, browse and locate the path to your `aptww` file.
|
||||
4. Click `Randomize` at the bottom-right. This randomizes the ISO and puts it in the output folder you specified. The
|
||||
3. For the APTWW file, browse and locate the path to your APTWW file.
|
||||
4. Click `Randomize` at the bottom right. This randomizes the ISO and puts it in the output folder you specified. The
|
||||
file will be named `TWW AP_YYYYY_P# (<name>).iso`, where `YYYYY` is the seed name, `#` is your player ID, and `<name>`
|
||||
is your player (slot) name. Verify that the values are correct for the multiworld.
|
||||
5. Open Dolphin and use it to open the randomized ISO.
|
||||
@@ -47,7 +49,7 @@ text client. If Dolphin is not already open, or you have yet to start a new file
|
||||
on the website, this will be `archipelago.gg:<port>`, where `<port>` is the port number. If a game is hosted from the
|
||||
`ArchipelagoServer.exe` (without `.exe` on Linux), the port number will default to `38281` but may be changed in the
|
||||
`host.yaml`.
|
||||
8. If you've opened a ROM corresponding to the multiworld to which you are connected, it should authenticate your slot
|
||||
8. If you've opened an ISO corresponding to the multiworld to which you are connected, it should authenticate your slot
|
||||
name automatically when you start a new save file.
|
||||
|
||||
## Troubleshooting
|
||||
@@ -55,13 +57,18 @@ name automatically when you start a new save file.
|
||||
* Ensure you are running the same version of Archipelago on which the multiworld was generated.
|
||||
* Ensure `tww.apworld` is not in your Archipelago installation's `custom_worlds` folder.
|
||||
* Ensure you are using the correct randomizer build for the version of Archipelago you are using. The build should
|
||||
provide an error message directing you to the correct version. You can also look at the release notes of TWW AP builds
|
||||
[here](https://github.com/tanjo3/wwrando/releases) to see which versions of Archipelago each build is compatible with.
|
||||
provide an error message directing you to the correct version. You can also look at the release notes of TWW AP builds
|
||||
[here](https://github.com/tanjo3/wwrando/releases?q=tag%3Aap_2) to see which versions of Archipelago each build is
|
||||
compatible with.
|
||||
* Do not run the Archipelago Launcher or Dolphin as an administrator on Windows.
|
||||
* If you encounter issues with authenticating, ensure that the randomized ROM is open in Dolphin and corresponds to the
|
||||
multiworld to which you are connecting.
|
||||
multiworld to which you are connecting.
|
||||
* Ensure that you do not have any Dolphin cheats or codes enabled. Some cheats or codes can unexpectedly interfere with
|
||||
emulation and make troubleshooting errors difficult.
|
||||
* If you get an error message, ensure that `Enable Emulated Memory Size Override` in Dolphin (under `Options` >
|
||||
`Configuration` > `Advanced`) is **disabled**.
|
||||
emulation and make troubleshooting errors difficult.
|
||||
* Ensure that `Enable Emulated Memory Size Override` in Dolphin (under `Options` > `Configuration` > `Advanced`) is
|
||||
**disabled**.
|
||||
* If the client cannot connect to Dolphin, ensure Dolphin is on the same drive as Archipelago. Having Dolphin on an
|
||||
external drive has reportedly caused connection issues.
|
||||
* Ensure the `Fallback Region` in Dolphin (under `Options` > `Configuration` > `General`) is set to `NTSC-U`.
|
||||
* If you run with a custom GC boot menu, you'll need to skip it by going to `Options` > `Configuration` > `GameCube`
|
||||
and checking `Skip Main Menu`.
|
||||
and checking `Skip Main Menu`.
|
||||
|
||||
Reference in New Issue
Block a user