mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-03-24 13:13:23 -07:00
Merge branch 'ArchipelagoMW:main' into main
This commit is contained in:
4
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
4
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
@@ -2,7 +2,7 @@ name: Bug Report
|
||||
description: File a bug report.
|
||||
title: "Bug: "
|
||||
labels:
|
||||
- bug
|
||||
- bug / fix
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
@@ -32,4 +32,4 @@ body:
|
||||
- Local generation
|
||||
- While playing
|
||||
validations:
|
||||
required: true
|
||||
required: true
|
||||
|
||||
17
.github/workflows/build.yml
vendored
17
.github/workflows/build.yml
vendored
@@ -4,6 +4,11 @@ name: Build
|
||||
|
||||
on: workflow_dispatch
|
||||
|
||||
env:
|
||||
SNI_VERSION: v0.0.84
|
||||
ENEMIZER_VERSION: 7.1
|
||||
APPIMAGETOOL_VERSION: 13
|
||||
|
||||
jobs:
|
||||
# build-release-macos: # LF volunteer
|
||||
|
||||
@@ -17,9 +22,9 @@ jobs:
|
||||
python-version: '3.8'
|
||||
- name: Download run-time dependencies
|
||||
run: |
|
||||
Invoke-WebRequest -Uri https://github.com/alttpo/sni/releases/download/v0.0.82/sni-v0.0.82-windows-amd64.zip -OutFile sni.zip
|
||||
Invoke-WebRequest -Uri https://github.com/alttpo/sni/releases/download/${Env:SNI_VERSION}/sni-${Env:SNI_VERSION}-windows-amd64.zip -OutFile sni.zip
|
||||
Expand-Archive -Path sni.zip -DestinationPath SNI -Force
|
||||
Invoke-WebRequest -Uri https://github.com/Ijwu/Enemizer/releases/download/7.0.1/win-x64.zip -OutFile enemizer.zip
|
||||
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
|
||||
- name: Build
|
||||
run: |
|
||||
@@ -43,6 +48,7 @@ jobs:
|
||||
build-ubuntu1804:
|
||||
runs-on: ubuntu-18.04
|
||||
steps:
|
||||
# - copy code below to release.yml -
|
||||
- uses: actions/checkout@v2
|
||||
- name: Install base dependencies
|
||||
run: |
|
||||
@@ -56,18 +62,18 @@ jobs:
|
||||
- name: Install build-time dependencies
|
||||
run: |
|
||||
echo "PYTHON=python3.9" >> $GITHUB_ENV
|
||||
wget -nv https://github.com/AppImage/AppImageKit/releases/download/13/appimagetool-x86_64.AppImage
|
||||
wget -nv https://github.com/AppImage/AppImageKit/releases/download/$APPIMAGETOOL_VERSION/appimagetool-x86_64.AppImage
|
||||
chmod a+rx appimagetool-x86_64.AppImage
|
||||
./appimagetool-x86_64.AppImage --appimage-extract
|
||||
echo -e '#/bin/sh\n./squashfs-root/AppRun "$@"' > appimagetool
|
||||
chmod a+rx appimagetool
|
||||
- name: Download run-time dependencies
|
||||
run: |
|
||||
wget -nv https://github.com/alttpo/sni/releases/download/v0.0.82/sni-v0.0.82-manylinux2014-amd64.tar.xz
|
||||
wget -nv https://github.com/alttpo/sni/releases/download/$SNI_VERSION/sni-$SNI_VERSION-manylinux2014-amd64.tar.xz
|
||||
tar xf sni-*.tar.xz
|
||||
rm sni-*.tar.xz
|
||||
mv sni-* SNI
|
||||
wget -nv https://github.com/Ijwu/Enemizer/releases/download/7.0.1/ubuntu.16.04-x64.7z
|
||||
wget -nv https://github.com/Ijwu/Enemizer/releases/download/$ENEMIZER_VERSION/ubuntu.16.04-x64.7z
|
||||
7za x -oEnemizerCLI/ ubuntu.16.04-x64.7z
|
||||
- name: Build
|
||||
run: |
|
||||
@@ -84,6 +90,7 @@ jobs:
|
||||
(cd build && DIR_NAME="`ls | grep exe`" && mv "$DIR_NAME" Archipelago && tar -czvf ../dist/$TAR_NAME Archipelago && mv Archipelago "$DIR_NAME")
|
||||
echo "APPIMAGE_NAME=$APPIMAGE_NAME" >> $GITHUB_ENV
|
||||
echo "TAR_NAME=$TAR_NAME" >> $GITHUB_ENV
|
||||
# - copy code above to release.yml -
|
||||
- name: Store AppImage
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
|
||||
4
.github/workflows/lint.yml
vendored
4
.github/workflows/lint.yml
vendored
@@ -18,8 +18,8 @@ jobs:
|
||||
python-version: 3.9
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install flake8 pytest
|
||||
python -m pip install --upgrade pip wheel
|
||||
pip install flake8 pytest pytest-subtests
|
||||
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
|
||||
- name: Lint with flake8
|
||||
run: |
|
||||
|
||||
14
.github/workflows/release.yml
vendored
14
.github/workflows/release.yml
vendored
@@ -7,6 +7,11 @@ on:
|
||||
tags:
|
||||
- '*.*.*'
|
||||
|
||||
env:
|
||||
SNI_VERSION: v0.0.84
|
||||
ENEMIZER_VERSION: 7.1
|
||||
APPIMAGETOOL_VERSION: 13
|
||||
|
||||
jobs:
|
||||
create-release:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -44,22 +49,23 @@ jobs:
|
||||
- name: Install build-time dependencies
|
||||
run: |
|
||||
echo "PYTHON=python3.9" >> $GITHUB_ENV
|
||||
wget -nv https://github.com/AppImage/AppImageKit/releases/download/13/appimagetool-x86_64.AppImage
|
||||
wget -nv https://github.com/AppImage/AppImageKit/releases/download/$APPIMAGETOOL_VERSION/appimagetool-x86_64.AppImage
|
||||
chmod a+rx appimagetool-x86_64.AppImage
|
||||
./appimagetool-x86_64.AppImage --appimage-extract
|
||||
echo -e '#/bin/sh\n./squashfs-root/AppRun "$@"' > appimagetool
|
||||
chmod a+rx appimagetool
|
||||
- name: Download run-time dependencies
|
||||
run: |
|
||||
wget -nv https://github.com/alttpo/sni/releases/download/v0.0.82/sni-v0.0.82-manylinux2014-amd64.tar.xz
|
||||
wget -nv https://github.com/alttpo/sni/releases/download/$SNI_VERSION/sni-$SNI_VERSION-manylinux2014-amd64.tar.xz
|
||||
tar xf sni-*.tar.xz
|
||||
rm sni-*.tar.xz
|
||||
mv sni-* SNI
|
||||
wget -nv https://github.com/Ijwu/Enemizer/releases/download/7.0.1/ubuntu.16.04-x64.7z
|
||||
wget -nv https://github.com/Ijwu/Enemizer/releases/download/$ENEMIZER_VERSION/ubuntu.16.04-x64.7z
|
||||
7za x -oEnemizerCLI/ ubuntu.16.04-x64.7z
|
||||
- name: Build
|
||||
run: |
|
||||
"${{ env.PYTHON }}" -m pip install --upgrade pip setuptools virtualenv PyGObject # pygobject should probably move to requirements
|
||||
# pygobject is an optional dependency for kivy that's not in requirements
|
||||
"${{ env.PYTHON }}" -m pip install --upgrade pip virtualenv PyGObject setuptools
|
||||
"${{ env.PYTHON }}" -m venv venv
|
||||
source venv/bin/activate
|
||||
pip install -r requirements.txt
|
||||
|
||||
4
.github/workflows/unittests.yml
vendored
4
.github/workflows/unittests.yml
vendored
@@ -32,8 +32,8 @@ jobs:
|
||||
python-version: ${{ matrix.python.version }}
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install flake8 pytest
|
||||
python -m pip install --upgrade pip wheel
|
||||
pip install flake8 pytest pytest-subtests
|
||||
python ModuleUpdate.py --yes --force --append "WebHostLib/requirements.txt"
|
||||
- name: Unittests
|
||||
run: |
|
||||
|
||||
@@ -1422,7 +1422,6 @@ class Spoiler():
|
||||
"f" in self.world.shop_shuffle[player]))
|
||||
outfile.write('Custom Potion Shop: %s\n' %
|
||||
bool_to_text("w" in self.world.shop_shuffle[player]))
|
||||
outfile.write('Boss shuffle: %s\n' % self.world.boss_shuffle[player])
|
||||
outfile.write('Enemy health: %s\n' % self.world.enemy_health[player])
|
||||
outfile.write('Enemy damage: %s\n' % self.world.enemy_damage[player])
|
||||
outfile.write('Prize shuffle %s\n' %
|
||||
|
||||
@@ -5,6 +5,7 @@ import urllib.parse
|
||||
import sys
|
||||
import typing
|
||||
import time
|
||||
import functools
|
||||
|
||||
import ModuleUpdate
|
||||
ModuleUpdate.update()
|
||||
@@ -17,7 +18,8 @@ if __name__ == "__main__":
|
||||
Utils.init_logging("TextClient", exception_logger="Client")
|
||||
|
||||
from MultiServer import CommandProcessor
|
||||
from NetUtils import Endpoint, decode, NetworkItem, encode, JSONtoTextParser, ClientStatus, Permission, NetworkSlot
|
||||
from NetUtils import Endpoint, decode, NetworkItem, encode, JSONtoTextParser, \
|
||||
ClientStatus, Permission, NetworkSlot, RawJSONtoTextParser
|
||||
from Utils import Version, stream_input
|
||||
from worlds import network_data_package, AutoWorldRegister
|
||||
import os
|
||||
@@ -152,8 +154,9 @@ class CommonContext:
|
||||
# locations
|
||||
locations_checked: typing.Set[int] # local state
|
||||
locations_scouted: typing.Set[int]
|
||||
missing_locations: typing.Set[int]
|
||||
missing_locations: typing.Set[int] # server state
|
||||
checked_locations: typing.Set[int] # server state
|
||||
server_locations: typing.Set[int] # all locations the server knows of, missing_location | checked_locations
|
||||
locations_info: typing.Dict[int, NetworkItem]
|
||||
|
||||
# internals
|
||||
@@ -184,8 +187,9 @@ class CommonContext:
|
||||
self.locations_checked = set() # local state
|
||||
self.locations_scouted = set()
|
||||
self.items_received = []
|
||||
self.missing_locations = set()
|
||||
self.missing_locations = set() # server state
|
||||
self.checked_locations = set() # server state
|
||||
self.server_locations = set() # all locations the server knows of, missing_location | checked_locations
|
||||
self.locations_info = {}
|
||||
|
||||
self.input_queue = asyncio.Queue()
|
||||
@@ -202,6 +206,10 @@ class CommonContext:
|
||||
# execution
|
||||
self.keep_alive_task = asyncio.create_task(keep_alive(self), name="Bouncy")
|
||||
|
||||
@functools.cached_property
|
||||
def raw_text_parser(self) -> RawJSONtoTextParser:
|
||||
return RawJSONtoTextParser(self)
|
||||
|
||||
@property
|
||||
def total_locations(self) -> typing.Optional[int]:
|
||||
"""Will return None until connected."""
|
||||
@@ -345,6 +353,8 @@ class CommonContext:
|
||||
cache_package = Utils.persistent_load().get("datapackage", {}).get("games", {})
|
||||
needed_updates: typing.Set[str] = set()
|
||||
for game in relevant_games:
|
||||
if game not in remote_datepackage_versions:
|
||||
continue
|
||||
remote_version: int = remote_datepackage_versions[game]
|
||||
|
||||
if remote_version == 0: # custom datapackage for this game
|
||||
@@ -632,6 +642,7 @@ async def process_server_cmd(ctx: CommonContext, args: dict):
|
||||
# when /missing is used for the client side view of what is missing.
|
||||
ctx.missing_locations = set(args["missing_locations"])
|
||||
ctx.checked_locations = set(args["checked_locations"])
|
||||
ctx.server_locations = ctx.missing_locations | ctx. checked_locations
|
||||
|
||||
elif cmd == 'ReceivedItems':
|
||||
start_index = args["index"]
|
||||
|
||||
53
FF1Client.py
53
FF1Client.py
@@ -1,4 +1,5 @@
|
||||
import asyncio
|
||||
import copy
|
||||
import json
|
||||
import time
|
||||
from asyncio import StreamReader, StreamWriter
|
||||
@@ -6,7 +7,7 @@ from typing import List
|
||||
|
||||
|
||||
import Utils
|
||||
from CommonClient import CommonContext, server_loop, gui_enabled, console_loop, ClientCommandProcessor, logger, \
|
||||
from CommonClient import CommonContext, server_loop, gui_enabled, ClientCommandProcessor, logger, \
|
||||
get_base_parser
|
||||
|
||||
SYSTEM_MESSAGE_ID = 0
|
||||
@@ -64,7 +65,7 @@ class FF1Context(CommonContext):
|
||||
|
||||
def _set_message(self, msg: str, msg_id: int):
|
||||
if DISPLAY_MSGS:
|
||||
self.messages[(time.time(), msg_id)] = msg
|
||||
self.messages[time.time(), msg_id] = msg
|
||||
|
||||
def on_package(self, cmd: str, args: dict):
|
||||
if cmd == 'Connected':
|
||||
@@ -73,32 +74,28 @@ class FF1Context(CommonContext):
|
||||
msg = args['text']
|
||||
if ': !' not in msg:
|
||||
self._set_message(msg, SYSTEM_MESSAGE_ID)
|
||||
elif cmd == "ReceivedItems":
|
||||
msg = f"Received {', '.join([self.item_names[item.item] for item in args['items']])}"
|
||||
self._set_message(msg, SYSTEM_MESSAGE_ID)
|
||||
elif cmd == 'PrintJSON':
|
||||
print_type = args['type']
|
||||
item = args['item']
|
||||
receiving_player_id = args['receiving']
|
||||
receiving_player_name = self.player_names[receiving_player_id]
|
||||
sending_player_id = item.player
|
||||
sending_player_name = self.player_names[item.player]
|
||||
if print_type == 'Hint':
|
||||
msg = f"Hint: Your {self.item_names[item.item]} is at" \
|
||||
f" {self.player_names[item.player]}'s {self.location_names[item.location]}"
|
||||
self._set_message(msg, item.item)
|
||||
elif print_type == 'ItemSend' and receiving_player_id != self.slot:
|
||||
if sending_player_id == self.slot:
|
||||
if receiving_player_id == self.slot:
|
||||
msg = f"You found your own {self.item_names[item.item]}"
|
||||
else:
|
||||
msg = f"You sent {self.item_names[item.item]} to {receiving_player_name}"
|
||||
else:
|
||||
if receiving_player_id == sending_player_id:
|
||||
msg = f"{sending_player_name} found their {self.item_names[item.item]}"
|
||||
else:
|
||||
msg = f"{sending_player_name} sent {self.item_names[item.item]} to " \
|
||||
f"{receiving_player_name}"
|
||||
|
||||
def on_print_json(self, args: dict):
|
||||
if self.ui:
|
||||
self.ui.print_json(copy.deepcopy(args["data"]))
|
||||
else:
|
||||
text = self.jsontotextparser(copy.deepcopy(args["data"]))
|
||||
logger.info(text)
|
||||
relevant = args.get("type", None) in {"Hint", "ItemSend"}
|
||||
if relevant:
|
||||
item = args["item"]
|
||||
# goes to this world
|
||||
if self.slot_concerns_self(args["receiving"]):
|
||||
relevant = True
|
||||
# found in this world
|
||||
elif self.slot_concerns_self(item.player):
|
||||
relevant = True
|
||||
# not related
|
||||
else:
|
||||
relevant = False
|
||||
if relevant:
|
||||
item = args["item"]
|
||||
msg = self.raw_text_parser(copy.deepcopy(args["data"]))
|
||||
self._set_message(msg, item.item)
|
||||
|
||||
def run_gui(self):
|
||||
|
||||
150
Fill.py
150
Fill.py
@@ -136,33 +136,98 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations:
|
||||
itempool.extend(unplaced_items)
|
||||
|
||||
|
||||
def remaining_fill(world: MultiWorld,
|
||||
locations: typing.List[Location],
|
||||
itempool: typing.List[Item]) -> None:
|
||||
unplaced_items: typing.List[Item] = []
|
||||
placements: typing.List[Location] = []
|
||||
swapped_items: typing.Counter[typing.Tuple[int, str]] = Counter()
|
||||
while locations and itempool:
|
||||
item_to_place = itempool.pop()
|
||||
spot_to_fill: typing.Optional[Location] = None
|
||||
|
||||
for i, location in enumerate(locations):
|
||||
if location.item_rule(item_to_place):
|
||||
# popping by index is faster than removing by content,
|
||||
spot_to_fill = locations.pop(i)
|
||||
# skipping a scan for the element
|
||||
break
|
||||
|
||||
else:
|
||||
# we filled all reachable spots.
|
||||
# try swapping this item with previously placed items
|
||||
|
||||
for (i, location) in enumerate(placements):
|
||||
placed_item = location.item
|
||||
# Unplaceable items can sometimes be swapped infinitely. Limit the
|
||||
# number of times we will swap an individual item to prevent this
|
||||
|
||||
if swapped_items[placed_item.player,
|
||||
placed_item.name] > 1:
|
||||
continue
|
||||
|
||||
location.item = None
|
||||
placed_item.location = None
|
||||
if location.item_rule(item_to_place):
|
||||
# Add this item to the existing placement, and
|
||||
# add the old item to the back of the queue
|
||||
spot_to_fill = placements.pop(i)
|
||||
|
||||
swapped_items[placed_item.player,
|
||||
placed_item.name] += 1
|
||||
|
||||
itempool.append(placed_item)
|
||||
|
||||
break
|
||||
|
||||
# Item can't be placed here, restore original item
|
||||
location.item = placed_item
|
||||
placed_item.location = location
|
||||
|
||||
if spot_to_fill is None:
|
||||
# Can't place this item, move on to the next
|
||||
unplaced_items.append(item_to_place)
|
||||
continue
|
||||
|
||||
world.push_item(spot_to_fill, item_to_place, False)
|
||||
placements.append(spot_to_fill)
|
||||
|
||||
if unplaced_items and locations:
|
||||
# There are leftover unplaceable items and locations that won't accept them
|
||||
raise FillError(f'No more spots to place {unplaced_items}, locations {locations} are invalid. '
|
||||
f'Already placed {len(placements)}: {", ".join(str(place) for place in placements)}')
|
||||
|
||||
itempool.extend(unplaced_items)
|
||||
|
||||
|
||||
def fast_fill(world: MultiWorld,
|
||||
item_pool: typing.List[Item],
|
||||
fill_locations: typing.List[Location]) -> typing.Tuple[typing.List[Item], typing.List[Location]]:
|
||||
placing = min(len(item_pool), len(fill_locations))
|
||||
for item, location in zip(item_pool, fill_locations):
|
||||
world.push_item(location, item, False)
|
||||
return item_pool[placing:], fill_locations[placing:]
|
||||
|
||||
|
||||
def distribute_items_restrictive(world: MultiWorld) -> None:
|
||||
fill_locations = sorted(world.get_unfilled_locations())
|
||||
world.random.shuffle(fill_locations)
|
||||
|
||||
# get items to distribute
|
||||
itempool = sorted(world.itempool)
|
||||
world.random.shuffle(itempool)
|
||||
progitempool: typing.List[Item] = []
|
||||
nonexcludeditempool: typing.List[Item] = []
|
||||
localrestitempool: typing.Dict[int, typing.List[Item]] = {player: [] for player in range(1, world.players + 1)}
|
||||
nonlocalrestitempool: typing.List[Item] = []
|
||||
restitempool: typing.List[Item] = []
|
||||
usefulitempool: typing.List[Item] = []
|
||||
filleritempool: typing.List[Item] = []
|
||||
|
||||
for item in itempool:
|
||||
if item.advancement:
|
||||
progitempool.append(item)
|
||||
elif item.useful: # this only gets nonprogression items which should not appear in excluded locations
|
||||
nonexcludeditempool.append(item)
|
||||
elif item.name in world.local_items[item.player].value:
|
||||
localrestitempool[item.player].append(item)
|
||||
elif item.name in world.non_local_items[item.player].value:
|
||||
nonlocalrestitempool.append(item)
|
||||
elif item.useful:
|
||||
usefulitempool.append(item)
|
||||
else:
|
||||
restitempool.append(item)
|
||||
filleritempool.append(item)
|
||||
|
||||
call_all(world, "fill_hook", progitempool, nonexcludeditempool,
|
||||
localrestitempool, nonlocalrestitempool, restitempool, fill_locations)
|
||||
call_all(world, "fill_hook", progitempool, usefulitempool, filleritempool, fill_locations)
|
||||
|
||||
locations: typing.Dict[LocationProgressType, typing.List[Location]] = {
|
||||
loc_type: [] for loc_type in LocationProgressType}
|
||||
@@ -184,50 +249,16 @@ def distribute_items_restrictive(world: MultiWorld) -> None:
|
||||
raise FillError(
|
||||
f'Not enough locations for progress items. There are {len(progitempool)} more items than locations')
|
||||
|
||||
if nonexcludeditempool:
|
||||
world.random.shuffle(defaultlocations)
|
||||
# needs logical fill to not conflict with local items
|
||||
fill_restrictive(
|
||||
world, world.state, defaultlocations, nonexcludeditempool)
|
||||
if nonexcludeditempool:
|
||||
raise FillError(
|
||||
f'Not enough locations for non-excluded items. There are {len(nonexcludeditempool)} more items than locations')
|
||||
remaining_fill(world, excludedlocations, filleritempool)
|
||||
if excludedlocations:
|
||||
raise FillError(
|
||||
f"Not enough filler items for excluded locations. There are {len(excludedlocations)} more locations than items")
|
||||
|
||||
defaultlocations = defaultlocations + excludedlocations
|
||||
world.random.shuffle(defaultlocations)
|
||||
restitempool = usefulitempool + filleritempool
|
||||
|
||||
if any(localrestitempool.values()): # we need to make sure some fills are limited to certain worlds
|
||||
local_locations: typing.Dict[int, typing.List[Location]] = {player: [] for player in world.player_ids}
|
||||
for location in defaultlocations:
|
||||
local_locations[location.player].append(location)
|
||||
for player_locations in local_locations.values():
|
||||
world.random.shuffle(player_locations)
|
||||
remaining_fill(world, defaultlocations, restitempool)
|
||||
|
||||
for player, items in localrestitempool.items(): # items already shuffled
|
||||
player_local_locations = local_locations[player]
|
||||
for item_to_place in items:
|
||||
if not player_local_locations:
|
||||
logging.warning(f"Ran out of local locations for player {player}, "
|
||||
f"cannot place {item_to_place}.")
|
||||
break
|
||||
spot_to_fill = player_local_locations.pop()
|
||||
world.push_item(spot_to_fill, item_to_place, False)
|
||||
defaultlocations.remove(spot_to_fill)
|
||||
|
||||
for item_to_place in nonlocalrestitempool:
|
||||
for i, location in enumerate(defaultlocations):
|
||||
if location.player != item_to_place.player:
|
||||
world.push_item(defaultlocations.pop(i), item_to_place, False)
|
||||
break
|
||||
else:
|
||||
raise Exception(f"Could not place non_local_item {item_to_place} among {defaultlocations}. "
|
||||
f"Too many non-local items for too few remaining locations.")
|
||||
|
||||
world.random.shuffle(defaultlocations)
|
||||
|
||||
restitempool, defaultlocations = fast_fill(
|
||||
world, restitempool, defaultlocations)
|
||||
unplaced = progitempool + restitempool
|
||||
unplaced = restitempool
|
||||
unfilled = defaultlocations
|
||||
|
||||
if unplaced or unfilled:
|
||||
@@ -241,15 +272,6 @@ def distribute_items_restrictive(world: MultiWorld) -> None:
|
||||
logging.info(f'Per-Player counts: {print_data})')
|
||||
|
||||
|
||||
def fast_fill(world: MultiWorld,
|
||||
item_pool: typing.List[Item],
|
||||
fill_locations: typing.List[Location]) -> typing.Tuple[typing.List[Item], typing.List[Location]]:
|
||||
placing = min(len(item_pool), len(fill_locations))
|
||||
for item, location in zip(item_pool, fill_locations):
|
||||
world.push_item(location, item, False)
|
||||
return item_pool[placing:], fill_locations[placing:]
|
||||
|
||||
|
||||
def flood_items(world: MultiWorld) -> None:
|
||||
# get items to distribute
|
||||
world.random.shuffle(world.itempool)
|
||||
|
||||
71
Generate.py
71
Generate.py
@@ -23,7 +23,6 @@ from worlds.alttp.EntranceRandomizer import parse_arguments
|
||||
from Main import main as ERmain
|
||||
from BaseClasses import seeddigits, get_seed
|
||||
import Options
|
||||
from worlds.alttp import Bosses
|
||||
from worlds.alttp.Text import TextTable
|
||||
from worlds.AutoWorld import AutoWorldRegister
|
||||
import copy
|
||||
@@ -63,7 +62,7 @@ class PlandoSettings(enum.IntFlag):
|
||||
|
||||
def __str__(self) -> str:
|
||||
if self.value:
|
||||
return ", ".join((flag.name for flag in PlandoSettings if self.value & flag.value))
|
||||
return ", ".join(flag.name for flag in PlandoSettings if self.value & flag.value)
|
||||
return "Off"
|
||||
|
||||
|
||||
@@ -84,11 +83,6 @@ def mystery_argparse():
|
||||
parser.add_argument('--seed', help='Define seed number to generate.', type=int)
|
||||
parser.add_argument('--multi', default=defaults["players"], type=lambda value: max(int(value), 1))
|
||||
parser.add_argument('--spoiler', type=int, default=defaults["spoiler"])
|
||||
parser.add_argument('--lttp_rom', default=options["lttp_options"]["rom_file"],
|
||||
help="Path to the 1.0 JP LttP Baserom.") # absolute, relative to cwd or relative to app path
|
||||
parser.add_argument('--sm_rom', default=options["sm_options"]["rom_file"],
|
||||
help="Path to the 1.0 JP SM Baserom.")
|
||||
parser.add_argument('--enemizercli', default=resolve_path(defaults["enemizer_path"], local_path))
|
||||
parser.add_argument('--outputpath', default=resolve_path(options["general_options"]["output_path"], user_path),
|
||||
help="Path to output folder. Absolute or relative to cwd.") # absolute or relative to cwd
|
||||
parser.add_argument('--race', action='store_true', default=defaults["race"])
|
||||
@@ -183,10 +177,6 @@ def main(args=None, callback=ERmain):
|
||||
|
||||
Utils.init_logging(f"Generate_{seed}", loglevel=args.log_level)
|
||||
|
||||
erargs.lttp_rom = args.lttp_rom
|
||||
erargs.sm_rom = args.sm_rom
|
||||
erargs.enemizercli = args.enemizercli
|
||||
|
||||
settings_cache: Dict[str, Tuple[argparse.Namespace, ...]] = \
|
||||
{fname: (tuple(roll_settings(yaml, args.plando) for yaml in yamls) if args.samesettings else None)
|
||||
for fname, yamls in weights_cache.items()}
|
||||
@@ -346,19 +336,6 @@ def prefer_int(input_data: str) -> Union[str, int]:
|
||||
return input_data
|
||||
|
||||
|
||||
available_boss_names: Set[str] = {boss.lower() for boss in Bosses.boss_table if boss not in
|
||||
{'Agahnim', 'Agahnim2', 'Ganon'}}
|
||||
available_boss_locations: Set[str] = {f"{loc.lower()}{f' {level}' if level else ''}" for loc, level in
|
||||
Bosses.boss_location_table}
|
||||
|
||||
boss_shuffle_options = {None: 'none',
|
||||
'none': 'none',
|
||||
'basic': 'basic',
|
||||
'full': 'full',
|
||||
'chaos': 'chaos',
|
||||
'singularity': 'singularity'
|
||||
}
|
||||
|
||||
goals = {
|
||||
'ganon': 'ganon',
|
||||
'crystals': 'crystals',
|
||||
@@ -465,42 +442,7 @@ def roll_triggers(weights: dict, triggers: list) -> dict:
|
||||
return weights
|
||||
|
||||
|
||||
def get_plando_bosses(boss_shuffle: str, plando_options: Set[str]) -> str:
|
||||
if boss_shuffle in boss_shuffle_options:
|
||||
return boss_shuffle_options[boss_shuffle]
|
||||
elif PlandoSettings.bosses in plando_options:
|
||||
options = boss_shuffle.lower().split(";")
|
||||
remainder_shuffle = "none" # vanilla
|
||||
bosses = []
|
||||
for boss in options:
|
||||
if boss in boss_shuffle_options:
|
||||
remainder_shuffle = boss_shuffle_options[boss]
|
||||
elif "-" in boss:
|
||||
loc, boss_name = boss.split("-")
|
||||
if boss_name not in available_boss_names:
|
||||
raise ValueError(f"Unknown Boss name {boss_name}")
|
||||
if loc not in available_boss_locations:
|
||||
raise ValueError(f"Unknown Boss Location {loc}")
|
||||
level = ''
|
||||
if loc.split(" ")[-1] in {"top", "middle", "bottom"}:
|
||||
# split off level
|
||||
loc = loc.split(" ")
|
||||
level = f" {loc[-1]}"
|
||||
loc = " ".join(loc[:-1])
|
||||
loc = loc.title().replace("Of", "of")
|
||||
if not Bosses.can_place_boss(boss_name.title(), loc, level):
|
||||
raise ValueError(f"Cannot place {boss_name} at {loc}{level}")
|
||||
bosses.append(boss)
|
||||
elif boss not in available_boss_names:
|
||||
raise ValueError(f"Unknown Boss name or Boss shuffle option {boss}.")
|
||||
else:
|
||||
bosses.append(boss)
|
||||
return ";".join(bosses + [remainder_shuffle])
|
||||
else:
|
||||
raise Exception(f"Boss Shuffle {boss_shuffle} is unknown and boss plando is turned off.")
|
||||
|
||||
|
||||
def handle_option(ret: argparse.Namespace, game_weights: dict, option_key: str, option: type(Options.Option)):
|
||||
def handle_option(ret: argparse.Namespace, game_weights: dict, option_key: str, option: type(Options.Option), plando_options: PlandoSettings):
|
||||
if option_key in game_weights:
|
||||
try:
|
||||
if not option.supports_weighting:
|
||||
@@ -511,8 +453,7 @@ def handle_option(ret: argparse.Namespace, game_weights: dict, option_key: str,
|
||||
except Exception as e:
|
||||
raise Exception(f"Error generating option {option_key} in {ret.game}") from e
|
||||
else:
|
||||
if hasattr(player_option, "verify"):
|
||||
player_option.verify(AutoWorldRegister.world_types[ret.game])
|
||||
player_option.verify(AutoWorldRegister.world_types[ret.game], ret.name, plando_options)
|
||||
else:
|
||||
setattr(ret, option_key, option(option.default))
|
||||
|
||||
@@ -558,11 +499,11 @@ def roll_settings(weights: dict, plando_options: PlandoSettings = PlandoSettings
|
||||
|
||||
if ret.game in AutoWorldRegister.world_types:
|
||||
for option_key, option in world_type.option_definitions.items():
|
||||
handle_option(ret, game_weights, option_key, option)
|
||||
handle_option(ret, game_weights, option_key, option, plando_options)
|
||||
for option_key, option in Options.per_game_common_options.items():
|
||||
# skip setting this option if already set from common_options, defaulting to root option
|
||||
if not (option_key in Options.common_options and option_key not in game_weights):
|
||||
handle_option(ret, game_weights, option_key, option)
|
||||
handle_option(ret, game_weights, option_key, option, plando_options)
|
||||
if PlandoSettings.items in plando_options:
|
||||
ret.plando_items = game_weights.get("plando_items", [])
|
||||
if ret.game == "Minecraft" or ret.game == "Ocarina of Time":
|
||||
@@ -645,8 +586,6 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
|
||||
|
||||
ret.item_functionality = get_choice_legacy('item_functionality', weights)
|
||||
|
||||
boss_shuffle = get_choice_legacy('boss_shuffle', weights)
|
||||
ret.shufflebosses = get_plando_bosses(boss_shuffle, plando_options)
|
||||
|
||||
ret.enemy_damage = {None: 'default',
|
||||
'default': 'default',
|
||||
|
||||
@@ -19,8 +19,9 @@ from os.path import isfile
|
||||
from shutil import which
|
||||
from typing import Iterable, Sequence, Callable, Union, Optional
|
||||
|
||||
import ModuleUpdate
|
||||
ModuleUpdate.update()
|
||||
if __name__ == "__main__":
|
||||
import ModuleUpdate
|
||||
ModuleUpdate.update()
|
||||
|
||||
from Utils import is_frozen, user_path, local_path, init_logging, open_filename, messagebox, \
|
||||
is_windows, is_macos, is_linux
|
||||
@@ -69,6 +70,7 @@ def browse_files():
|
||||
webbrowser.open(file)
|
||||
|
||||
|
||||
# noinspection PyArgumentList
|
||||
class Type(Enum):
|
||||
TOOL = auto()
|
||||
FUNC = auto() # not a real component
|
||||
|
||||
@@ -83,9 +83,9 @@ def main():
|
||||
parser.add_argument('--ow_palettes', default='default',
|
||||
choices=['default', 'random', 'blackout', 'puke', 'classic', 'grayscale', 'negative', 'dizzy',
|
||||
'sick'])
|
||||
parser.add_argument('--link_palettes', default='default',
|
||||
choices=['default', 'random', 'blackout', 'puke', 'classic', 'grayscale', 'negative', 'dizzy',
|
||||
'sick'])
|
||||
# parser.add_argument('--link_palettes', default='default',
|
||||
# choices=['default', 'random', 'blackout', 'puke', 'classic', 'grayscale', 'negative', 'dizzy',
|
||||
# 'sick'])
|
||||
parser.add_argument('--shield_palettes', default='default',
|
||||
choices=['default', 'random', 'blackout', 'puke', 'classic', 'grayscale', 'negative', 'dizzy',
|
||||
'sick'])
|
||||
@@ -752,6 +752,7 @@ class SpriteSelector():
|
||||
self.window['pady'] = 5
|
||||
self.spritesPerRow = 32
|
||||
self.all_sprites = []
|
||||
self.invalid_sprites = []
|
||||
self.sprite_pool = spritePool
|
||||
|
||||
def open_custom_sprite_dir(_evt):
|
||||
@@ -833,6 +834,13 @@ class SpriteSelector():
|
||||
self.window.focus()
|
||||
tkinter_center_window(self.window)
|
||||
|
||||
if self.invalid_sprites:
|
||||
invalid = sorted(self.invalid_sprites)
|
||||
logging.warning(f"The following sprites are invalid: {', '.join(invalid)}")
|
||||
msg = f"{invalid[0]} "
|
||||
msg += f"and {len(invalid)-1} more are invalid" if len(invalid) > 1 else "is invalid"
|
||||
messagebox.showerror("Invalid sprites detected", msg, parent=self.window)
|
||||
|
||||
def remove_from_sprite_pool(self, button, spritename):
|
||||
self.callback(("remove", spritename))
|
||||
self.spritePoolButtons.buttons.remove(button)
|
||||
@@ -897,7 +905,13 @@ class SpriteSelector():
|
||||
sprites = []
|
||||
|
||||
for file in os.listdir(path):
|
||||
sprites.append((file, Sprite(os.path.join(path, file))))
|
||||
if file == '.gitignore':
|
||||
continue
|
||||
sprite = Sprite(os.path.join(path, file))
|
||||
if sprite.valid:
|
||||
sprites.append((file, sprite))
|
||||
else:
|
||||
self.invalid_sprites.append(file)
|
||||
|
||||
sprites.sort(key=lambda s: str.lower(s[1].name or "").strip())
|
||||
|
||||
|
||||
1
Main.py
1
Main.py
@@ -70,7 +70,6 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
||||
world.required_medallions = args.required_medallions.copy()
|
||||
world.game = args.game.copy()
|
||||
world.player_name = args.name.copy()
|
||||
world.enemizer = args.enemizercli
|
||||
world.sprite = args.sprite.copy()
|
||||
world.glitch_triforce = args.glitch_triforce # This is enabled/disabled globally, no per player option.
|
||||
|
||||
|
||||
@@ -593,6 +593,7 @@ class Context:
|
||||
forfeit_player(self, client.team, client.slot)
|
||||
elif self.forced_auto_forfeits[self.games[client.slot]]:
|
||||
forfeit_player(self, client.team, client.slot)
|
||||
self.save() # save goal completion flag
|
||||
|
||||
|
||||
def notify_hints(ctx: Context, team: int, hints: typing.List[NetUtils.Hint], only_new: bool = False):
|
||||
@@ -742,6 +743,7 @@ async def countdown(ctx: Context, timer: int):
|
||||
broadcast_countdown(ctx, 0, f"[Server]: GO")
|
||||
ctx.countdown_timer = 0
|
||||
|
||||
|
||||
def broadcast_text_all(ctx: Context, text: str, additional_arguments: dict = {}):
|
||||
old_clients, new_clients = [], []
|
||||
|
||||
@@ -754,8 +756,10 @@ def broadcast_text_all(ctx: Context, text: str, additional_arguments: dict = {})
|
||||
ctx.broadcast(old_clients, [{"cmd": "Print", "text": text }])
|
||||
ctx.broadcast(new_clients, [{**{"cmd": "PrintJSON", "data": [{ "text": text }]}, **additional_arguments}])
|
||||
|
||||
|
||||
def broadcast_countdown(ctx: Context, timer: int, message: str):
|
||||
broadcast_text_all(ctx, message, { "type": "Countdown", "countdown": timer })
|
||||
broadcast_text_all(ctx, message, {"type": "Countdown", "countdown": timer})
|
||||
|
||||
|
||||
def get_players_string(ctx: Context):
|
||||
auth_clients = {(c.team, c.slot) for c in ctx.endpoints if c.auth}
|
||||
@@ -2040,15 +2044,28 @@ async def main(args: argparse.Namespace):
|
||||
args.auto_shutdown, args.compatibility, args.log_network)
|
||||
data_filename = args.multidata
|
||||
|
||||
try:
|
||||
if not data_filename:
|
||||
if not data_filename:
|
||||
try:
|
||||
filetypes = (("Multiworld data", (".archipelago", ".zip")),)
|
||||
data_filename = Utils.open_filename("Select multiworld data", filetypes)
|
||||
|
||||
except Exception as e:
|
||||
if isinstance(e, ImportError) or (e.__class__.__name__ == "TclError" and "no display" in str(e)):
|
||||
if not isinstance(e, ImportError):
|
||||
logging.error(f"Failed to load tkinter ({e})")
|
||||
logging.info("Pass a multidata filename on command line to run headless.")
|
||||
exit(1)
|
||||
raise
|
||||
|
||||
if not data_filename:
|
||||
logging.info("No file selected. Exiting.")
|
||||
exit(1)
|
||||
|
||||
try:
|
||||
ctx.load(data_filename, args.use_embedded_options)
|
||||
|
||||
except Exception as e:
|
||||
logging.exception('Failed to read multiworld data (%s)' % e)
|
||||
logging.exception(f"Failed to read multiworld data ({e})")
|
||||
raise
|
||||
|
||||
ctx.init_save(not args.disable_save)
|
||||
|
||||
106
Options.py
106
Options.py
@@ -26,15 +26,31 @@ class AssembleOptions(abc.ABCMeta):
|
||||
|
||||
attrs["name_lookup"].update({option_id: name for name, option_id in new_options.items()})
|
||||
options.update(new_options)
|
||||
|
||||
# apply aliases, without name_lookup
|
||||
aliases = {name[6:].lower(): option_id for name, option_id in attrs.items() if
|
||||
name.startswith("alias_")}
|
||||
|
||||
assert "random" not in aliases, "Choice option 'random' cannot be manually assigned."
|
||||
|
||||
# auto-alias Off and On being parsed as True and False
|
||||
if "off" in options:
|
||||
options["false"] = options["off"]
|
||||
if "on" in options:
|
||||
options["true"] = options["on"]
|
||||
|
||||
options.update(aliases)
|
||||
|
||||
if "verify" not in attrs:
|
||||
# not overridden by class -> look up bases
|
||||
verifiers = [f for f in (getattr(base, "verify", None) for base in bases) if f]
|
||||
if len(verifiers) > 1: # verify multiple bases/mixins
|
||||
def verify(self, *args, **kwargs) -> None:
|
||||
for f in verifiers:
|
||||
f(self, *args, **kwargs)
|
||||
attrs["verify"] = verify
|
||||
else:
|
||||
assert verifiers, "class Option is supposed to implement def verify"
|
||||
|
||||
# auto-validate schema on __init__
|
||||
if "schema" in attrs.keys():
|
||||
|
||||
@@ -112,6 +128,41 @@ class Option(typing.Generic[T], metaclass=AssembleOptions):
|
||||
def from_any(cls, data: typing.Any) -> Option[T]:
|
||||
raise NotImplementedError
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from Generate import PlandoSettings
|
||||
from worlds.AutoWorld import World
|
||||
|
||||
def verify(self, world: World, player_name: str, plando_options: PlandoSettings) -> None:
|
||||
pass
|
||||
else:
|
||||
def verify(self, *args, **kwargs) -> None:
|
||||
pass
|
||||
|
||||
|
||||
class FreeText(Option):
|
||||
"""Text option that allows users to enter strings.
|
||||
Needs to be validated by the world or option definition."""
|
||||
|
||||
def __init__(self, value: str):
|
||||
assert isinstance(value, str), "value of FreeText must be a string"
|
||||
self.value = value
|
||||
|
||||
@property
|
||||
def current_key(self) -> str:
|
||||
return self.value
|
||||
|
||||
@classmethod
|
||||
def from_text(cls, text: str) -> FreeText:
|
||||
return cls(text)
|
||||
|
||||
@classmethod
|
||||
def from_any(cls, data: typing.Any) -> FreeText:
|
||||
return cls.from_text(str(data))
|
||||
|
||||
@classmethod
|
||||
def get_option_name(cls, value: T) -> str:
|
||||
return value
|
||||
|
||||
|
||||
class NumericOption(Option[int], numbers.Integral):
|
||||
# note: some of the `typing.Any`` here is a result of unresolved issue in python standards
|
||||
@@ -368,6 +419,53 @@ class Choice(NumericOption):
|
||||
__hash__ = Option.__hash__ # see https://docs.python.org/3/reference/datamodel.html#object.__hash__
|
||||
|
||||
|
||||
class TextChoice(Choice):
|
||||
"""Allows custom string input and offers choices. Choices will resolve to int and text will resolve to string"""
|
||||
|
||||
def __init__(self, value: typing.Union[str, int]):
|
||||
assert isinstance(value, str) or isinstance(value, int), \
|
||||
f"{value} is not a valid option for {self.__class__.__name__}"
|
||||
self.value = value
|
||||
super(TextChoice, self).__init__()
|
||||
|
||||
@property
|
||||
def current_key(self) -> str:
|
||||
if isinstance(self.value, str):
|
||||
return self.value
|
||||
else:
|
||||
return self.name_lookup[self.value]
|
||||
|
||||
@classmethod
|
||||
def from_text(cls, text: str) -> TextChoice:
|
||||
if text.lower() == "random": # chooses a random defined option but won't use any free text options
|
||||
return cls(random.choice(list(cls.name_lookup)))
|
||||
for option_name, value in cls.options.items():
|
||||
if option_name.lower() == text.lower():
|
||||
return cls(value)
|
||||
return cls(text)
|
||||
|
||||
@classmethod
|
||||
def get_option_name(cls, value: T) -> str:
|
||||
if isinstance(value, str):
|
||||
return value
|
||||
return cls.name_lookup[value]
|
||||
|
||||
def __eq__(self, other: typing.Any):
|
||||
if isinstance(other, self.__class__):
|
||||
return other.value == self.value
|
||||
elif isinstance(other, str):
|
||||
if other in self.options:
|
||||
return other == self.current_key
|
||||
return other == self.value
|
||||
elif isinstance(other, int):
|
||||
assert other in self.name_lookup, f"compared against an int that could never be equal. {self} == {other}"
|
||||
return other == self.value
|
||||
elif isinstance(other, bool):
|
||||
return other == bool(self.value)
|
||||
else:
|
||||
raise TypeError(f"Can't compare {self.__class__.__name__} with {other.__class__.__name__}")
|
||||
|
||||
|
||||
class Range(NumericOption):
|
||||
range_start = 0
|
||||
range_end = 1
|
||||
@@ -507,7 +605,7 @@ class VerifyKeys:
|
||||
raise Exception(f"Found unexpected key {', '.join(extra)} in {cls}. "
|
||||
f"Allowed keys: {cls.valid_keys}.")
|
||||
|
||||
def verify(self, world):
|
||||
def verify(self, world, player_name: str, plando_options) -> None:
|
||||
if self.convert_name_groups and self.verify_item_name:
|
||||
new_value = type(self.value)() # empty container of whatever value is
|
||||
for item_name in self.value:
|
||||
@@ -732,8 +830,8 @@ class ItemLinks(OptionList):
|
||||
pool |= {item_name}
|
||||
return pool
|
||||
|
||||
def verify(self, world):
|
||||
super(ItemLinks, self).verify(world)
|
||||
def verify(self, world, player_name: str, plando_options) -> None:
|
||||
super(ItemLinks, self).verify(world, player_name, plando_options)
|
||||
existing_links = set()
|
||||
for link in self.value:
|
||||
if link["name"] in existing_links:
|
||||
|
||||
3
Patch.py
3
Patch.py
@@ -17,7 +17,7 @@ ModuleUpdate.update()
|
||||
|
||||
import Utils
|
||||
|
||||
current_patch_version = 4
|
||||
current_patch_version = 5
|
||||
|
||||
|
||||
class AutoPatchRegister(type):
|
||||
@@ -128,6 +128,7 @@ class APDeltaPatch(APContainer, metaclass=AutoPatchRegister):
|
||||
manifest = super(APDeltaPatch, self).get_manifest()
|
||||
manifest["base_checksum"] = self.hash
|
||||
manifest["result_file_ending"] = self.result_file_ending
|
||||
manifest["patch_file_ending"] = self.patch_file_ending
|
||||
return manifest
|
||||
|
||||
@classmethod
|
||||
|
||||
59
SNIClient.py
59
SNIClient.py
@@ -15,9 +15,6 @@ import typing
|
||||
|
||||
from json import loads, dumps
|
||||
|
||||
import ModuleUpdate
|
||||
ModuleUpdate.update()
|
||||
|
||||
from Utils import init_logging, messagebox
|
||||
|
||||
if __name__ == "__main__":
|
||||
@@ -149,8 +146,8 @@ class Context(CommonContext):
|
||||
def event_invalid_slot(self):
|
||||
if self.snes_socket is not None and not self.snes_socket.closed:
|
||||
asyncio.create_task(self.snes_socket.close())
|
||||
raise Exception('Invalid ROM detected, '
|
||||
'please verify that you have loaded the correct rom and reconnect your snes (/snes)')
|
||||
raise Exception("Invalid ROM detected, "
|
||||
"please verify that you have loaded the correct rom and reconnect your snes (/snes)")
|
||||
|
||||
async def server_auth(self, password_requested: bool = False):
|
||||
if password_requested and not self.password:
|
||||
@@ -158,7 +155,7 @@ class Context(CommonContext):
|
||||
if self.rom is None:
|
||||
self.awaiting_rom = True
|
||||
snes_logger.info(
|
||||
'No ROM detected, awaiting snes connection to authenticate to the multiworld server (/snes)')
|
||||
"No ROM detected, awaiting snes connection to authenticate to the multiworld server (/snes)")
|
||||
return
|
||||
self.awaiting_rom = False
|
||||
self.auth = self.rom
|
||||
@@ -262,7 +259,7 @@ async def deathlink_kill_player(ctx: Context):
|
||||
|
||||
SNES_RECONNECT_DELAY = 5
|
||||
|
||||
# LttP
|
||||
# FXPAK Pro protocol memory mapping used by SNI
|
||||
ROM_START = 0x000000
|
||||
WRAM_START = 0xF50000
|
||||
WRAM_SIZE = 0x20000
|
||||
@@ -293,21 +290,24 @@ SHOP_LEN = (len(Shops.shop_table) * 3) + 5
|
||||
DEATH_LINK_ACTIVE_ADDR = ROMNAME_START + 0x15 # 1 byte
|
||||
|
||||
# SM
|
||||
SM_ROMNAME_START = 0x007FC0
|
||||
SM_ROMNAME_START = ROM_START + 0x007FC0
|
||||
|
||||
SM_INGAME_MODES = {0x07, 0x09, 0x0b}
|
||||
SM_ENDGAME_MODES = {0x26, 0x27}
|
||||
SM_DEATH_MODES = {0x15, 0x17, 0x18, 0x19, 0x1A}
|
||||
|
||||
SM_RECV_PROGRESS_ADDR = SRAM_START + 0x2000 # 2 bytes
|
||||
SM_RECV_ITEM_ADDR = SAVEDATA_START + 0x4D2 # 1 byte
|
||||
SM_RECV_ITEM_PLAYER_ADDR = SAVEDATA_START + 0x4D3 # 1 byte
|
||||
# RECV and SEND are from the gameplay's perspective: SNIClient writes to RECV queue and reads from SEND queue
|
||||
SM_RECV_QUEUE_START = SRAM_START + 0x2000
|
||||
SM_RECV_QUEUE_WCOUNT = SRAM_START + 0x2602
|
||||
SM_SEND_QUEUE_START = SRAM_START + 0x2700
|
||||
SM_SEND_QUEUE_RCOUNT = SRAM_START + 0x2680
|
||||
SM_SEND_QUEUE_WCOUNT = SRAM_START + 0x2682
|
||||
|
||||
SM_DEATH_LINK_ACTIVE_ADDR = ROM_START + 0x277f04 # 1 byte
|
||||
SM_REMOTE_ITEM_FLAG_ADDR = ROM_START + 0x277f06 # 1 byte
|
||||
|
||||
# SMZ3
|
||||
SMZ3_ROMNAME_START = 0x00FFC0
|
||||
SMZ3_ROMNAME_START = ROM_START + 0x00FFC0
|
||||
|
||||
SMZ3_INGAME_MODES = {0x07, 0x09, 0x0b}
|
||||
SMZ3_ENDGAME_MODES = {0x26, 0x27}
|
||||
@@ -1083,6 +1083,9 @@ async def game_watcher(ctx: Context):
|
||||
|
||||
if ctx.awaiting_rom:
|
||||
await ctx.server_auth(False)
|
||||
elif ctx.server is None:
|
||||
snes_logger.warning("ROM detected but no active multiworld server connection. " +
|
||||
"Connect using command: /connect server:port")
|
||||
|
||||
if ctx.auth and ctx.auth != ctx.rom:
|
||||
snes_logger.warning("ROM change detected, please reconnect to the multiworld server")
|
||||
@@ -1159,6 +1162,9 @@ async def game_watcher(ctx: Context):
|
||||
await ctx.send_msgs([{"cmd": "LocationScouts", "locations": [scout_location]}])
|
||||
await track_locations(ctx, roomid, roomdata)
|
||||
elif ctx.game == GAME_SM:
|
||||
if ctx.server is None or ctx.slot is None:
|
||||
# not successfully connected to a multiworld server, cannot process the game sending items
|
||||
continue
|
||||
gamemode = await snes_read(ctx, WRAM_START + 0x0998, 1)
|
||||
if "DeathLink" in ctx.tags and gamemode and ctx.last_death_link + 1 < time.time():
|
||||
currently_dead = gamemode[0] in SM_DEATH_MODES
|
||||
@@ -1169,25 +1175,25 @@ async def game_watcher(ctx: Context):
|
||||
ctx.finished_game = True
|
||||
continue
|
||||
|
||||
data = await snes_read(ctx, SM_RECV_PROGRESS_ADDR + 0x680, 4)
|
||||
data = await snes_read(ctx, SM_SEND_QUEUE_RCOUNT, 4)
|
||||
if data is None:
|
||||
continue
|
||||
|
||||
recv_index = data[0] | (data[1] << 8)
|
||||
recv_item = data[2] | (data[3] << 8)
|
||||
recv_item = data[2] | (data[3] << 8) # this is actually SM_SEND_QUEUE_WCOUNT
|
||||
|
||||
while (recv_index < recv_item):
|
||||
itemAdress = recv_index * 8
|
||||
message = await snes_read(ctx, SM_RECV_PROGRESS_ADDR + 0x700 + itemAdress, 8)
|
||||
message = await snes_read(ctx, SM_SEND_QUEUE_START + itemAdress, 8)
|
||||
# worldId = message[0] | (message[1] << 8) # unused
|
||||
# itemId = message[2] | (message[3] << 8) # unused
|
||||
itemIndex = (message[4] | (message[5] << 8)) >> 3
|
||||
|
||||
recv_index += 1
|
||||
snes_buffered_write(ctx, SM_RECV_PROGRESS_ADDR + 0x680,
|
||||
snes_buffered_write(ctx, SM_SEND_QUEUE_RCOUNT,
|
||||
bytes([recv_index & 0xFF, (recv_index >> 8) & 0xFF]))
|
||||
|
||||
from worlds.sm.Locations import locations_start_id
|
||||
from worlds.sm import locations_start_id
|
||||
location_id = locations_start_id + itemIndex
|
||||
|
||||
ctx.locations_checked.add(location_id)
|
||||
@@ -1196,15 +1202,14 @@ async def game_watcher(ctx: Context):
|
||||
f'New Check: {location} ({len(ctx.locations_checked)}/{len(ctx.missing_locations) + len(ctx.checked_locations)})')
|
||||
await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": [location_id]}])
|
||||
|
||||
data = await snes_read(ctx, SM_RECV_PROGRESS_ADDR + 0x600, 4)
|
||||
data = await snes_read(ctx, SM_RECV_QUEUE_WCOUNT, 2)
|
||||
if data is None:
|
||||
continue
|
||||
|
||||
# recv_itemOutPtr = data[0] | (data[1] << 8) # unused
|
||||
itemOutPtr = data[2] | (data[3] << 8)
|
||||
itemOutPtr = data[0] | (data[1] << 8)
|
||||
|
||||
from worlds.sm.Items import items_start_id
|
||||
from worlds.sm.Locations import locations_start_id
|
||||
from worlds.sm import items_start_id
|
||||
from worlds.sm import locations_start_id
|
||||
if itemOutPtr < len(ctx.items_received):
|
||||
item = ctx.items_received[itemOutPtr]
|
||||
itemId = item.item - items_start_id
|
||||
@@ -1214,10 +1219,10 @@ async def game_watcher(ctx: Context):
|
||||
locationId = 0x00 #backward compat
|
||||
|
||||
playerID = item.player if item.player <= SM_ROM_PLAYER_LIMIT else 0
|
||||
snes_buffered_write(ctx, SM_RECV_PROGRESS_ADDR + itemOutPtr * 4, bytes(
|
||||
snes_buffered_write(ctx, SM_RECV_QUEUE_START + itemOutPtr * 4, bytes(
|
||||
[playerID & 0xFF, (playerID >> 8) & 0xFF, itemId & 0xFF, locationId & 0xFF]))
|
||||
itemOutPtr += 1
|
||||
snes_buffered_write(ctx, SM_RECV_PROGRESS_ADDR + 0x602,
|
||||
snes_buffered_write(ctx, SM_RECV_QUEUE_WCOUNT,
|
||||
bytes([itemOutPtr & 0xFF, (itemOutPtr >> 8) & 0xFF]))
|
||||
logging.info('Received %s from %s (%s) (%d/%d in list)' % (
|
||||
color(ctx.item_names[item.item], 'red', 'bold'),
|
||||
@@ -1225,6 +1230,9 @@ async def game_watcher(ctx: Context):
|
||||
ctx.location_names[item.location], itemOutPtr, len(ctx.items_received)))
|
||||
await snes_flush_writes(ctx)
|
||||
elif ctx.game == GAME_SMZ3:
|
||||
if ctx.server is None or ctx.slot is None:
|
||||
# not successfully connected to a multiworld server, cannot process the game sending items
|
||||
continue
|
||||
currentGame = await snes_read(ctx, SRAM_START + 0x33FE, 2)
|
||||
if (currentGame is not None):
|
||||
if (currentGame[0] != 0):
|
||||
@@ -1260,7 +1268,8 @@ async def game_watcher(ctx: Context):
|
||||
snes_buffered_write(ctx, SMZ3_RECV_PROGRESS_ADDR + 0x680, bytes([recv_index & 0xFF, (recv_index >> 8) & 0xFF]))
|
||||
|
||||
from worlds.smz3.TotalSMZ3.Location import locations_start_id
|
||||
location_id = locations_start_id + itemIndex
|
||||
from worlds.smz3 import convertLocSMZ3IDToAPID
|
||||
location_id = locations_start_id + convertLocSMZ3IDToAPID(itemIndex)
|
||||
|
||||
ctx.locations_checked.add(location_id)
|
||||
location = ctx.location_names[location_id]
|
||||
|
||||
@@ -1,31 +1,32 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import multiprocessing
|
||||
import logging
|
||||
import asyncio
|
||||
import copy
|
||||
import ctypes
|
||||
import logging
|
||||
import multiprocessing
|
||||
import os.path
|
||||
import re
|
||||
import sys
|
||||
import typing
|
||||
import queue
|
||||
from pathlib import Path
|
||||
|
||||
import nest_asyncio
|
||||
import sc2
|
||||
|
||||
from sc2.main import run_game
|
||||
from sc2.data import Race
|
||||
from sc2.bot_ai import BotAI
|
||||
from sc2.data import Race
|
||||
from sc2.main import run_game
|
||||
from sc2.player import Bot
|
||||
|
||||
from worlds.sc2wol.Regions import MissionInfo
|
||||
from worlds.sc2wol.MissionTables import lookup_id_to_mission
|
||||
from worlds.sc2wol.Items import lookup_id_to_name, item_table
|
||||
from worlds.sc2wol.Locations import SC2WOL_LOC_ID_OFFSET
|
||||
from worlds.sc2wol import SC2WoLWorld
|
||||
|
||||
from pathlib import Path
|
||||
import re
|
||||
import NetUtils
|
||||
from MultiServer import mark_raw
|
||||
import ctypes
|
||||
import sys
|
||||
|
||||
from Utils import init_logging, is_windows
|
||||
from worlds.sc2wol import SC2WoLWorld
|
||||
from worlds.sc2wol.Items import lookup_id_to_name, item_table, ItemData, type_flaggroups
|
||||
from worlds.sc2wol.Locations import SC2WOL_LOC_ID_OFFSET
|
||||
from worlds.sc2wol.MissionTables import lookup_id_to_mission
|
||||
from worlds.sc2wol.Regions import MissionInfo
|
||||
|
||||
if __name__ == "__main__":
|
||||
init_logging("SC2Client", exception_logger="Client")
|
||||
@@ -35,10 +36,12 @@ sc2_logger = logging.getLogger("Starcraft2")
|
||||
|
||||
import colorama
|
||||
|
||||
from NetUtils import *
|
||||
from NetUtils import ClientStatus, RawJSONtoTextParser
|
||||
from CommonClient import CommonContext, server_loop, ClientCommandProcessor, gui_enabled, get_base_parser
|
||||
|
||||
nest_asyncio.apply()
|
||||
max_bonus: int = 8
|
||||
victory_modulo: int = 100
|
||||
|
||||
|
||||
class StarcraftClientProcessor(ClientCommandProcessor):
|
||||
@@ -98,13 +101,13 @@ class StarcraftClientProcessor(ClientCommandProcessor):
|
||||
def _cmd_available(self) -> bool:
|
||||
"""Get what missions are currently available to play"""
|
||||
|
||||
request_available_missions(self.ctx.checked_locations, self.ctx.mission_req_table, self.ctx.ui)
|
||||
request_available_missions(self.ctx)
|
||||
return True
|
||||
|
||||
def _cmd_unfinished(self) -> bool:
|
||||
"""Get what missions are currently available to play and have not had all locations checked"""
|
||||
|
||||
request_unfinished_missions(self.ctx.checked_locations, self.ctx.mission_req_table, self.ctx.ui, self.ctx)
|
||||
request_unfinished_missions(self.ctx)
|
||||
return True
|
||||
|
||||
@mark_raw
|
||||
@@ -125,18 +128,19 @@ class SC2Context(CommonContext):
|
||||
items_handling = 0b111
|
||||
difficulty = -1
|
||||
all_in_choice = 0
|
||||
mission_req_table = None
|
||||
items_rec_to_announce = []
|
||||
rec_announce_pos = 0
|
||||
items_sent_to_announce = []
|
||||
sent_announce_pos = 0
|
||||
announcements = []
|
||||
announcement_pos = 0
|
||||
mission_req_table: typing.Dict[str, MissionInfo] = {}
|
||||
announcements = queue.Queue()
|
||||
sc2_run_task: typing.Optional[asyncio.Task] = None
|
||||
missions_unlocked = False
|
||||
missions_unlocked: bool = False # allow launching missions ignoring requirements
|
||||
current_tooltip = None
|
||||
last_loc_list = None
|
||||
difficulty_override = -1
|
||||
mission_id_to_location_ids: typing.Dict[int, typing.List[int]] = {}
|
||||
last_bot: typing.Optional[ArchipelagoBot] = None
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(SC2Context, self).__init__(*args, **kwargs)
|
||||
self.raw_text_parser = RawJSONtoTextParser(self)
|
||||
|
||||
async def server_auth(self, password_requested: bool = False):
|
||||
if password_requested and not self.password:
|
||||
@@ -149,30 +153,35 @@ class SC2Context(CommonContext):
|
||||
self.difficulty = args["slot_data"]["game_difficulty"]
|
||||
self.all_in_choice = args["slot_data"]["all_in_map"]
|
||||
slot_req_table = args["slot_data"]["mission_req"]
|
||||
self.mission_req_table = {}
|
||||
# Compatibility for 0.3.2 server data.
|
||||
if "category" not in next(iter(slot_req_table)):
|
||||
for i, mission_data in enumerate(slot_req_table.values()):
|
||||
mission_data["category"] = wol_default_categories[i]
|
||||
for mission in slot_req_table:
|
||||
self.mission_req_table[mission] = MissionInfo(**slot_req_table[mission])
|
||||
self.mission_req_table = {
|
||||
mission: MissionInfo(**slot_req_table[mission]) for mission in slot_req_table
|
||||
}
|
||||
|
||||
self.build_location_to_mission_mapping()
|
||||
|
||||
# Look for and set SC2PATH.
|
||||
# check_game_install_path() returns True if and only if it finds + sets SC2PATH.
|
||||
if "SC2PATH" not in os.environ and check_game_install_path():
|
||||
check_mod_install()
|
||||
|
||||
if cmd in {"PrintJSON"}:
|
||||
if "receiving" in args:
|
||||
if self.slot_concerns_self(args["receiving"]):
|
||||
self.announcements.append(args["data"])
|
||||
return
|
||||
if "item" in args:
|
||||
if self.slot_concerns_self(args["item"].player):
|
||||
self.announcements.append(args["data"])
|
||||
def on_print_json(self, args: dict):
|
||||
# goes to this world
|
||||
if "receiving" in args and self.slot_concerns_self(args["receiving"]):
|
||||
relevant = True
|
||||
# found in this world
|
||||
elif "item" in args and self.slot_concerns_self(args["item"].player):
|
||||
relevant = True
|
||||
# not related
|
||||
else:
|
||||
relevant = False
|
||||
|
||||
if relevant:
|
||||
self.announcements.put(self.raw_text_parser(copy.deepcopy(args["data"])))
|
||||
|
||||
super(SC2Context, self).on_print_json(args)
|
||||
|
||||
def run_gui(self):
|
||||
from kvui import GameManager, HoverBehavior, ServerToolTip, fade_in_animation
|
||||
from kvui import GameManager, HoverBehavior, ServerToolTip
|
||||
from kivy.app import App
|
||||
from kivy.clock import Clock
|
||||
from kivy.uix.tabbedpanel import TabbedPanelItem
|
||||
@@ -190,6 +199,7 @@ class SC2Context(CommonContext):
|
||||
|
||||
class MissionButton(HoverableButton):
|
||||
tooltip_text = StringProperty("Test")
|
||||
ctx: SC2Context
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(HoverableButton, self).__init__(*args, **kwargs)
|
||||
@@ -210,10 +220,7 @@ class SC2Context(CommonContext):
|
||||
self.ctx.current_tooltip = self.layout
|
||||
|
||||
def on_leave(self):
|
||||
if self.ctx.current_tooltip:
|
||||
App.get_running_app().root.remove_widget(self.ctx.current_tooltip)
|
||||
|
||||
self.ctx.current_tooltip = None
|
||||
self.ctx.ui.clear_tooltip()
|
||||
|
||||
@property
|
||||
def ctx(self) -> CommonContext:
|
||||
@@ -235,13 +242,20 @@ class SC2Context(CommonContext):
|
||||
mission_panel = None
|
||||
last_checked_locations = {}
|
||||
mission_id_to_button = {}
|
||||
launching = False
|
||||
launching: typing.Union[bool, int] = False # if int -> mission ID
|
||||
refresh_from_launching = True
|
||||
first_check = True
|
||||
ctx: SC2Context
|
||||
|
||||
def __init__(self, ctx):
|
||||
super().__init__(ctx)
|
||||
|
||||
def clear_tooltip(self):
|
||||
if self.ctx.current_tooltip:
|
||||
App.get_running_app().root.remove_widget(self.ctx.current_tooltip)
|
||||
|
||||
self.ctx.current_tooltip = None
|
||||
|
||||
def build(self):
|
||||
container = super().build()
|
||||
|
||||
@@ -256,7 +270,7 @@ class SC2Context(CommonContext):
|
||||
|
||||
def build_mission_table(self, dt):
|
||||
if (not self.launching and (not self.last_checked_locations == self.ctx.checked_locations or
|
||||
not self.refresh_from_launching)) or self.first_check:
|
||||
not self.refresh_from_launching)) or self.first_check:
|
||||
self.refresh_from_launching = True
|
||||
|
||||
self.mission_panel.clear_widgets()
|
||||
@@ -267,12 +281,7 @@ class SC2Context(CommonContext):
|
||||
|
||||
self.mission_id_to_button = {}
|
||||
categories = {}
|
||||
available_missions = []
|
||||
unfinished_locations = initialize_blank_mission_dict(self.ctx.mission_req_table)
|
||||
unfinished_missions = calc_unfinished_missions(self.ctx.checked_locations,
|
||||
self.ctx.mission_req_table,
|
||||
self.ctx, available_missions=available_missions,
|
||||
unfinished_locations=unfinished_locations)
|
||||
available_missions, unfinished_missions = calc_unfinished_missions(self.ctx)
|
||||
|
||||
# separate missions into categories
|
||||
for mission in self.ctx.mission_req_table:
|
||||
@@ -283,7 +292,8 @@ class SC2Context(CommonContext):
|
||||
|
||||
for category in categories:
|
||||
category_panel = MissionCategory()
|
||||
category_panel.add_widget(Label(text=category, size_hint_y=None, height=50, outline_width=1))
|
||||
category_panel.add_widget(
|
||||
Label(text=category, size_hint_y=None, height=50, outline_width=1))
|
||||
|
||||
# Map is completed
|
||||
for mission in categories[category]:
|
||||
@@ -295,7 +305,9 @@ class SC2Context(CommonContext):
|
||||
text = f"[color=6495ED]{text}[/color]"
|
||||
|
||||
tooltip = f"Uncollected locations:\n"
|
||||
tooltip += "\n".join(location for location in unfinished_locations[mission])
|
||||
tooltip += "\n".join([self.ctx.location_names[loc] for loc in
|
||||
self.ctx.locations_for_mission(mission)
|
||||
if loc in self.ctx.missing_locations])
|
||||
elif mission in available_missions:
|
||||
text = f"[color=FFFFFF]{text}[/color]"
|
||||
# Map requirements not met
|
||||
@@ -303,7 +315,7 @@ class SC2Context(CommonContext):
|
||||
text = f"[color=a9a9a9]{text}[/color]"
|
||||
tooltip = f"Requires: "
|
||||
if len(self.ctx.mission_req_table[mission].required_world) > 0:
|
||||
tooltip += ", ".join(list(self.ctx.mission_req_table)[req_mission-1] for
|
||||
tooltip += ", ".join(list(self.ctx.mission_req_table)[req_mission - 1] for
|
||||
req_mission in
|
||||
self.ctx.mission_req_table[mission].required_world)
|
||||
|
||||
@@ -325,13 +337,16 @@ class SC2Context(CommonContext):
|
||||
self.refresh_from_launching = False
|
||||
|
||||
self.mission_panel.clear_widgets()
|
||||
self.mission_panel.add_widget(Label(text="Launching Mission"))
|
||||
self.mission_panel.add_widget(Label(text="Launching Mission: " +
|
||||
lookup_id_to_mission[self.launching]))
|
||||
if self.ctx.ui:
|
||||
self.ctx.ui.clear_tooltip()
|
||||
|
||||
def mission_callback(self, button):
|
||||
if not self.launching:
|
||||
self.ctx.play_mission(list(self.mission_id_to_button.keys())
|
||||
[list(self.mission_id_to_button.values()).index(button)])
|
||||
self.launching = True
|
||||
mission_id: int = next(k for k, v in self.mission_id_to_button.items() if v == button)
|
||||
self.ctx.play_mission(mission_id)
|
||||
self.launching = mission_id
|
||||
Clock.schedule_once(self.finish_launching, 10)
|
||||
|
||||
def finish_launching(self, dt):
|
||||
@@ -344,12 +359,14 @@ class SC2Context(CommonContext):
|
||||
|
||||
async def shutdown(self):
|
||||
await super(SC2Context, self).shutdown()
|
||||
if self.last_bot:
|
||||
self.last_bot.want_close = True
|
||||
if self.sc2_run_task:
|
||||
self.sc2_run_task.cancel()
|
||||
|
||||
def play_mission(self, mission_id):
|
||||
def play_mission(self, mission_id: int):
|
||||
if self.missions_unlocked or \
|
||||
is_mission_available(mission_id, self.checked_locations, self.mission_req_table):
|
||||
is_mission_available(self, mission_id):
|
||||
if self.sc2_run_task:
|
||||
if not self.sc2_run_task.done():
|
||||
sc2_logger.warning("Starcraft 2 Client is still running!")
|
||||
@@ -358,12 +375,29 @@ class SC2Context(CommonContext):
|
||||
sc2_logger.warning("Launching Mission without Archipelago authentication, "
|
||||
"checks will not be registered to server.")
|
||||
self.sc2_run_task = asyncio.create_task(starcraft_launch(self, mission_id),
|
||||
name="Starcraft 2 Launch")
|
||||
name="Starcraft 2 Launch")
|
||||
else:
|
||||
sc2_logger.info(
|
||||
f"{lookup_id_to_mission[mission_id]} is not currently unlocked. "
|
||||
f"Use /unfinished or /available to see what is available.")
|
||||
|
||||
def build_location_to_mission_mapping(self):
|
||||
mission_id_to_location_ids: typing.Dict[int, typing.Set[int]] = {
|
||||
mission_info.id: set() for mission_info in self.mission_req_table.values()
|
||||
}
|
||||
|
||||
for loc in self.server_locations:
|
||||
mission_id, objective = divmod(loc - SC2WOL_LOC_ID_OFFSET, victory_modulo)
|
||||
mission_id_to_location_ids[mission_id].add(objective)
|
||||
self.mission_id_to_location_ids = {mission_id: sorted(objectives) for mission_id, objectives in
|
||||
mission_id_to_location_ids.items()}
|
||||
|
||||
def locations_for_mission(self, mission: str):
|
||||
mission_id: int = self.mission_req_table[mission].id
|
||||
objectives = self.mission_id_to_location_ids[self.mission_req_table[mission].id]
|
||||
for objective in objectives:
|
||||
yield SC2WOL_LOC_ID_OFFSET + mission_id * 100 + objective
|
||||
|
||||
|
||||
async def main():
|
||||
multiprocessing.freeze_support()
|
||||
@@ -403,47 +437,27 @@ wol_default_categories = [
|
||||
]
|
||||
|
||||
|
||||
def calculate_items(items):
|
||||
unit_unlocks = 0
|
||||
armory1_unlocks = 0
|
||||
armory2_unlocks = 0
|
||||
upgrade_unlocks = 0
|
||||
building_unlocks = 0
|
||||
merc_unlocks = 0
|
||||
lab_unlocks = 0
|
||||
protoss_unlock = 0
|
||||
minerals = 0
|
||||
vespene = 0
|
||||
supply = 0
|
||||
def calculate_items(items: typing.List[NetUtils.NetworkItem]) -> typing.List[int]:
|
||||
network_item: NetUtils.NetworkItem
|
||||
accumulators: typing.List[int] = [0 for _ in type_flaggroups]
|
||||
|
||||
for item in items:
|
||||
data = lookup_id_to_name[item.item]
|
||||
for network_item in items:
|
||||
name: str = lookup_id_to_name[network_item.item]
|
||||
item_data: ItemData = item_table[name]
|
||||
|
||||
if item_table[data].type == "Unit":
|
||||
unit_unlocks += (1 << item_table[data].number)
|
||||
elif item_table[data].type == "Upgrade":
|
||||
upgrade_unlocks += (1 << item_table[data].number)
|
||||
elif item_table[data].type == "Armory 1":
|
||||
armory1_unlocks += (1 << item_table[data].number)
|
||||
elif item_table[data].type == "Armory 2":
|
||||
armory2_unlocks += (1 << item_table[data].number)
|
||||
elif item_table[data].type == "Building":
|
||||
building_unlocks += (1 << item_table[data].number)
|
||||
elif item_table[data].type == "Mercenary":
|
||||
merc_unlocks += (1 << item_table[data].number)
|
||||
elif item_table[data].type == "Laboratory":
|
||||
lab_unlocks += (1 << item_table[data].number)
|
||||
elif item_table[data].type == "Protoss":
|
||||
protoss_unlock += (1 << item_table[data].number)
|
||||
elif item_table[data].type == "Minerals":
|
||||
minerals += item_table[data].number
|
||||
elif item_table[data].type == "Vespene":
|
||||
vespene += item_table[data].number
|
||||
elif item_table[data].type == "Supply":
|
||||
supply += item_table[data].number
|
||||
# exists exactly once
|
||||
if item_data.quantity == 1:
|
||||
accumulators[type_flaggroups[item_data.type]] |= 1 << item_data.number
|
||||
|
||||
return [unit_unlocks, upgrade_unlocks, armory1_unlocks, armory2_unlocks, building_unlocks, merc_unlocks,
|
||||
lab_unlocks, protoss_unlock, minerals, vespene, supply]
|
||||
# exists multiple times
|
||||
elif item_data.type == "Upgrade":
|
||||
accumulators[type_flaggroups[item_data.type]] += 1 << item_data.number
|
||||
|
||||
# sum
|
||||
else:
|
||||
accumulators[type_flaggroups[item_data.type]] += item_data.number
|
||||
|
||||
return accumulators
|
||||
|
||||
|
||||
def calc_difficulty(difficulty):
|
||||
@@ -459,11 +473,7 @@ def calc_difficulty(difficulty):
|
||||
return 'X'
|
||||
|
||||
|
||||
async def starcraft_launch(ctx: SC2Context, mission_id):
|
||||
ctx.rec_announce_pos = len(ctx.items_rec_to_announce)
|
||||
ctx.sent_announce_pos = len(ctx.items_sent_to_announce)
|
||||
ctx.announcements_pos = len(ctx.announcements)
|
||||
|
||||
async def starcraft_launch(ctx: SC2Context, mission_id: int):
|
||||
sc2_logger.info(f"Launching {lookup_id_to_mission[mission_id]}. If game does not launch check log file for errors.")
|
||||
|
||||
with DllDirectory(None):
|
||||
@@ -472,32 +482,34 @@ async def starcraft_launch(ctx: SC2Context, mission_id):
|
||||
|
||||
|
||||
class ArchipelagoBot(sc2.bot_ai.BotAI):
|
||||
game_running = False
|
||||
mission_completed = False
|
||||
first_bonus = False
|
||||
second_bonus = False
|
||||
third_bonus = False
|
||||
fourth_bonus = False
|
||||
fifth_bonus = False
|
||||
sixth_bonus = False
|
||||
seventh_bonus = False
|
||||
eight_bonus = False
|
||||
ctx: SC2Context = None
|
||||
mission_id = 0
|
||||
|
||||
game_running: bool = False
|
||||
mission_completed: bool = False
|
||||
boni: typing.List[bool]
|
||||
setup_done: bool
|
||||
ctx: SC2Context
|
||||
mission_id: int
|
||||
want_close: bool = False
|
||||
can_read_game = False
|
||||
|
||||
last_received_update = 0
|
||||
last_received_update: int = 0
|
||||
|
||||
def __init__(self, ctx: SC2Context, mission_id):
|
||||
self.setup_done = False
|
||||
self.ctx = ctx
|
||||
self.ctx.last_bot = self
|
||||
self.mission_id = mission_id
|
||||
self.boni = [False for _ in range(max_bonus)]
|
||||
|
||||
super(ArchipelagoBot, self).__init__()
|
||||
|
||||
async def on_step(self, iteration: int):
|
||||
if self.want_close:
|
||||
self.want_close = False
|
||||
await self._client.leave()
|
||||
return
|
||||
game_state = 0
|
||||
if iteration == 0:
|
||||
if not self.setup_done:
|
||||
self.setup_done = True
|
||||
start_items = calculate_items(self.ctx.items_received)
|
||||
if self.ctx.difficulty_override >= 0:
|
||||
difficulty = calc_difficulty(self.ctx.difficulty_override)
|
||||
@@ -511,36 +523,10 @@ class ArchipelagoBot(sc2.bot_ai.BotAI):
|
||||
self.last_received_update = len(self.ctx.items_received)
|
||||
|
||||
else:
|
||||
if self.ctx.announcement_pos < len(self.ctx.announcements):
|
||||
index = 0
|
||||
message = ""
|
||||
while index < len(self.ctx.announcements[self.ctx.announcement_pos]):
|
||||
message += self.ctx.announcements[self.ctx.announcement_pos][index]["text"]
|
||||
index += 1
|
||||
|
||||
index = 0
|
||||
start_rem_pos = -1
|
||||
# Remove unneeded [Color] tags
|
||||
while index < len(message):
|
||||
if message[index] == '[':
|
||||
start_rem_pos = index
|
||||
index += 1
|
||||
elif message[index] == ']' and start_rem_pos > -1:
|
||||
temp_msg = ""
|
||||
|
||||
if start_rem_pos > 0:
|
||||
temp_msg = message[:start_rem_pos]
|
||||
if index < len(message) - 1:
|
||||
temp_msg += message[index + 1:]
|
||||
|
||||
message = temp_msg
|
||||
index += start_rem_pos - index
|
||||
start_rem_pos = -1
|
||||
else:
|
||||
index += 1
|
||||
|
||||
if not self.ctx.announcements.empty():
|
||||
message = self.ctx.announcements.get(timeout=1)
|
||||
await self.chat_send("SendMessage " + message)
|
||||
self.ctx.announcement_pos += 1
|
||||
self.ctx.announcements.task_done()
|
||||
|
||||
# Archipelago reads the health
|
||||
for unit in self.all_own_units():
|
||||
@@ -568,169 +554,97 @@ class ArchipelagoBot(sc2.bot_ai.BotAI):
|
||||
if game_state & (1 << 1) and not self.mission_completed:
|
||||
if self.mission_id != 29:
|
||||
print("Mission Completed")
|
||||
await self.ctx.send_msgs([
|
||||
{"cmd": 'LocationChecks', "locations": [SC2WOL_LOC_ID_OFFSET + 100 * self.mission_id]}])
|
||||
await self.ctx.send_msgs(
|
||||
[{"cmd": 'LocationChecks',
|
||||
"locations": [SC2WOL_LOC_ID_OFFSET + victory_modulo * self.mission_id]}])
|
||||
self.mission_completed = True
|
||||
else:
|
||||
print("Game Complete")
|
||||
await self.ctx.send_msgs([{"cmd": 'StatusUpdate', "status": ClientStatus.CLIENT_GOAL}])
|
||||
self.mission_completed = True
|
||||
|
||||
if game_state & (1 << 2) and not self.first_bonus:
|
||||
print("1st Bonus Collected")
|
||||
await self.ctx.send_msgs(
|
||||
[{"cmd": 'LocationChecks',
|
||||
"locations": [SC2WOL_LOC_ID_OFFSET + 100 * self.mission_id + 1]}])
|
||||
self.first_bonus = True
|
||||
|
||||
if not self.second_bonus and game_state & (1 << 3):
|
||||
print("2nd Bonus Collected")
|
||||
await self.ctx.send_msgs(
|
||||
[{"cmd": 'LocationChecks',
|
||||
"locations": [SC2WOL_LOC_ID_OFFSET + 100 * self.mission_id + 2]}])
|
||||
self.second_bonus = True
|
||||
|
||||
if not self.third_bonus and game_state & (1 << 4):
|
||||
print("3rd Bonus Collected")
|
||||
await self.ctx.send_msgs(
|
||||
[{"cmd": 'LocationChecks',
|
||||
"locations": [SC2WOL_LOC_ID_OFFSET + 100 * self.mission_id + 3]}])
|
||||
self.third_bonus = True
|
||||
|
||||
if not self.fourth_bonus and game_state & (1 << 5):
|
||||
print("4th Bonus Collected")
|
||||
await self.ctx.send_msgs(
|
||||
[{"cmd": 'LocationChecks',
|
||||
"locations": [SC2WOL_LOC_ID_OFFSET + 100 * self.mission_id + 4]}])
|
||||
self.fourth_bonus = True
|
||||
|
||||
if not self.fifth_bonus and game_state & (1 << 6):
|
||||
print("5th Bonus Collected")
|
||||
await self.ctx.send_msgs(
|
||||
[{"cmd": 'LocationChecks',
|
||||
"locations": [SC2WOL_LOC_ID_OFFSET + 100 * self.mission_id + 5]}])
|
||||
self.fifth_bonus = True
|
||||
|
||||
if not self.sixth_bonus and game_state & (1 << 7):
|
||||
print("6th Bonus Collected")
|
||||
await self.ctx.send_msgs(
|
||||
[{"cmd": 'LocationChecks',
|
||||
"locations": [SC2WOL_LOC_ID_OFFSET + 100 * self.mission_id + 6]}])
|
||||
self.sixth_bonus = True
|
||||
|
||||
if not self.seventh_bonus and game_state & (1 << 8):
|
||||
print("6th Bonus Collected")
|
||||
await self.ctx.send_msgs(
|
||||
[{"cmd": 'LocationChecks',
|
||||
"locations": [SC2WOL_LOC_ID_OFFSET + 100 * self.mission_id + 7]}])
|
||||
self.seventh_bonus = True
|
||||
|
||||
if not self.eight_bonus and game_state & (1 << 9):
|
||||
print("6th Bonus Collected")
|
||||
await self.ctx.send_msgs(
|
||||
[{"cmd": 'LocationChecks',
|
||||
"locations": [SC2WOL_LOC_ID_OFFSET + 100 * self.mission_id + 8]}])
|
||||
self.eight_bonus = True
|
||||
for x, completed in enumerate(self.boni):
|
||||
if not completed and game_state & (1 << (x + 2)):
|
||||
await self.ctx.send_msgs(
|
||||
[{"cmd": 'LocationChecks',
|
||||
"locations": [SC2WOL_LOC_ID_OFFSET + victory_modulo * self.mission_id + x + 1]}])
|
||||
self.boni[x] = True
|
||||
|
||||
else:
|
||||
await self.chat_send("LostConnection - Lost connection to game.")
|
||||
|
||||
|
||||
def calc_objectives_completed(mission, missions_info, locations_done, unfinished_locations, ctx):
|
||||
objectives_complete = 0
|
||||
|
||||
if missions_info[mission].extra_locations > 0:
|
||||
for i in range(missions_info[mission].extra_locations):
|
||||
if (missions_info[mission].id * 100 + SC2WOL_LOC_ID_OFFSET + i) in locations_done:
|
||||
objectives_complete += 1
|
||||
else:
|
||||
unfinished_locations[mission].append(ctx.location_names[
|
||||
missions_info[mission].id * 100 + SC2WOL_LOC_ID_OFFSET + i])
|
||||
|
||||
return objectives_complete
|
||||
|
||||
else:
|
||||
return -1
|
||||
|
||||
|
||||
def request_unfinished_missions(locations_done, location_table, ui, ctx):
|
||||
if location_table:
|
||||
def request_unfinished_missions(ctx: SC2Context):
|
||||
if ctx.mission_req_table:
|
||||
message = "Unfinished Missions: "
|
||||
unlocks = initialize_blank_mission_dict(location_table)
|
||||
unfinished_locations = initialize_blank_mission_dict(location_table)
|
||||
unlocks = initialize_blank_mission_dict(ctx.mission_req_table)
|
||||
unfinished_locations = initialize_blank_mission_dict(ctx.mission_req_table)
|
||||
|
||||
unfinished_missions = calc_unfinished_missions(locations_done, location_table, ctx, unlocks=unlocks,
|
||||
unfinished_locations=unfinished_locations)
|
||||
_, unfinished_missions = calc_unfinished_missions(ctx, unlocks=unlocks)
|
||||
|
||||
message += ", ".join(f"{mark_up_mission_name(mission, location_table, ui,unlocks)}[{location_table[mission].id}] " +
|
||||
message += ", ".join(f"{mark_up_mission_name(ctx, mission, unlocks)}[{ctx.mission_req_table[mission].id}] " +
|
||||
mark_up_objectives(
|
||||
f"[{unfinished_missions[mission]}/{location_table[mission].extra_locations}]",
|
||||
f"[{len(unfinished_missions[mission])}/"
|
||||
f"{sum(1 for _ in ctx.locations_for_mission(mission))}]",
|
||||
ctx, unfinished_locations, mission)
|
||||
for mission in unfinished_missions)
|
||||
|
||||
if ui:
|
||||
ui.log_panels['All'].on_message_markup(message)
|
||||
ui.log_panels['Starcraft2'].on_message_markup(message)
|
||||
if ctx.ui:
|
||||
ctx.ui.log_panels['All'].on_message_markup(message)
|
||||
ctx.ui.log_panels['Starcraft2'].on_message_markup(message)
|
||||
else:
|
||||
sc2_logger.info(message)
|
||||
else:
|
||||
sc2_logger.warning("No mission table found, you are likely not connected to a server.")
|
||||
|
||||
|
||||
def calc_unfinished_missions(locations_done, locations, ctx, unlocks=None, unfinished_locations=None,
|
||||
available_missions=[]):
|
||||
def calc_unfinished_missions(ctx: SC2Context, unlocks=None):
|
||||
unfinished_missions = []
|
||||
locations_completed = []
|
||||
|
||||
if not unlocks:
|
||||
unlocks = initialize_blank_mission_dict(locations)
|
||||
unlocks = initialize_blank_mission_dict(ctx.mission_req_table)
|
||||
|
||||
if not unfinished_locations:
|
||||
unfinished_locations = initialize_blank_mission_dict(locations)
|
||||
|
||||
if len(available_missions) > 0:
|
||||
available_missions = []
|
||||
|
||||
available_missions.extend(calc_available_missions(locations_done, locations, unlocks))
|
||||
available_missions = calc_available_missions(ctx, unlocks)
|
||||
|
||||
for name in available_missions:
|
||||
if not locations[name].extra_locations == -1:
|
||||
objectives_completed = calc_objectives_completed(name, locations, locations_done, unfinished_locations, ctx)
|
||||
|
||||
if objectives_completed < locations[name].extra_locations:
|
||||
objectives = set(ctx.locations_for_mission(name))
|
||||
if objectives:
|
||||
objectives_completed = ctx.checked_locations & objectives
|
||||
if len(objectives_completed) < len(objectives):
|
||||
unfinished_missions.append(name)
|
||||
locations_completed.append(objectives_completed)
|
||||
|
||||
else:
|
||||
else: # infer that this is the final mission as it has no objectives
|
||||
unfinished_missions.append(name)
|
||||
locations_completed.append(-1)
|
||||
|
||||
return {unfinished_missions[i]: locations_completed[i] for i in range(len(unfinished_missions))}
|
||||
return available_missions, dict(zip(unfinished_missions, locations_completed))
|
||||
|
||||
|
||||
def is_mission_available(mission_id_to_check, locations_done, locations):
|
||||
unfinished_missions = calc_available_missions(locations_done, locations)
|
||||
def is_mission_available(ctx: SC2Context, mission_id_to_check):
|
||||
unfinished_missions = calc_available_missions(ctx)
|
||||
|
||||
return any(mission_id_to_check == locations[mission].id for mission in unfinished_missions)
|
||||
return any(mission_id_to_check == ctx.mission_req_table[mission].id for mission in unfinished_missions)
|
||||
|
||||
|
||||
def mark_up_mission_name(mission, location_table, ui, unlock_table):
|
||||
def mark_up_mission_name(ctx: SC2Context, mission, unlock_table):
|
||||
"""Checks if the mission is required for game completion and adds '*' to the name to mark that."""
|
||||
|
||||
if location_table[mission].completion_critical:
|
||||
if ui:
|
||||
if ctx.mission_req_table[mission].completion_critical:
|
||||
if ctx.ui:
|
||||
message = "[color=AF99EF]" + mission + "[/color]"
|
||||
else:
|
||||
message = "*" + mission + "*"
|
||||
else:
|
||||
message = mission
|
||||
|
||||
if ui:
|
||||
if ctx.ui:
|
||||
unlocks = unlock_table[mission]
|
||||
|
||||
if len(unlocks) > 0:
|
||||
pre_message = f"[ref={list(location_table).index(mission)}|Unlocks: "
|
||||
pre_message += ", ".join(f"{unlock}({location_table[unlock].id})" for unlock in unlocks)
|
||||
pre_message = f"[ref={list(ctx.mission_req_table).index(mission)}|Unlocks: "
|
||||
pre_message += ", ".join(f"{unlock}({ctx.mission_req_table[unlock].id})" for unlock in unlocks)
|
||||
pre_message += f"]"
|
||||
message = pre_message + message + "[/ref]"
|
||||
|
||||
@@ -743,7 +657,7 @@ def mark_up_objectives(message, ctx, unfinished_locations, mission):
|
||||
if ctx.ui:
|
||||
locations = unfinished_locations[mission]
|
||||
|
||||
pre_message = f"[ref={list(ctx.mission_req_table).index(mission)+30}|"
|
||||
pre_message = f"[ref={list(ctx.mission_req_table).index(mission) + 30}|"
|
||||
pre_message += "<br>".join(location for location in locations)
|
||||
pre_message += f"]"
|
||||
formatted_message = pre_message + message + "[/ref]"
|
||||
@@ -751,90 +665,91 @@ def mark_up_objectives(message, ctx, unfinished_locations, mission):
|
||||
return formatted_message
|
||||
|
||||
|
||||
def request_available_missions(locations_done, location_table, ui):
|
||||
if location_table:
|
||||
def request_available_missions(ctx: SC2Context):
|
||||
if ctx.mission_req_table:
|
||||
message = "Available Missions: "
|
||||
|
||||
# Initialize mission unlock table
|
||||
unlocks = initialize_blank_mission_dict(location_table)
|
||||
unlocks = initialize_blank_mission_dict(ctx.mission_req_table)
|
||||
|
||||
missions = calc_available_missions(locations_done, location_table, unlocks)
|
||||
missions = calc_available_missions(ctx, unlocks)
|
||||
message += \
|
||||
", ".join(f"{mark_up_mission_name(mission, location_table, ui, unlocks)}[{location_table[mission].id}]"
|
||||
", ".join(f"{mark_up_mission_name(ctx, mission, unlocks)}"
|
||||
f"[{ctx.mission_req_table[mission].id}]"
|
||||
for mission in missions)
|
||||
|
||||
if ui:
|
||||
ui.log_panels['All'].on_message_markup(message)
|
||||
ui.log_panels['Starcraft2'].on_message_markup(message)
|
||||
if ctx.ui:
|
||||
ctx.ui.log_panels['All'].on_message_markup(message)
|
||||
ctx.ui.log_panels['Starcraft2'].on_message_markup(message)
|
||||
else:
|
||||
sc2_logger.info(message)
|
||||
else:
|
||||
sc2_logger.warning("No mission table found, you are likely not connected to a server.")
|
||||
|
||||
|
||||
def calc_available_missions(locations_done, locations, unlocks=None):
|
||||
def calc_available_missions(ctx: SC2Context, unlocks=None):
|
||||
available_missions = []
|
||||
missions_complete = 0
|
||||
|
||||
# Get number of missions completed
|
||||
for loc in locations_done:
|
||||
if loc % 100 == 0:
|
||||
for loc in ctx.checked_locations:
|
||||
if loc % victory_modulo == 0:
|
||||
missions_complete += 1
|
||||
|
||||
for name in locations:
|
||||
for name in ctx.mission_req_table:
|
||||
# Go through the required missions for each mission and fill up unlock table used later for hover-over tooltips
|
||||
if unlocks:
|
||||
for unlock in locations[name].required_world:
|
||||
unlocks[list(locations)[unlock-1]].append(name)
|
||||
for unlock in ctx.mission_req_table[name].required_world:
|
||||
unlocks[list(ctx.mission_req_table)[unlock - 1]].append(name)
|
||||
|
||||
if mission_reqs_completed(name, missions_complete, locations_done, locations):
|
||||
if mission_reqs_completed(ctx, name, missions_complete):
|
||||
available_missions.append(name)
|
||||
|
||||
return available_missions
|
||||
|
||||
|
||||
def mission_reqs_completed(location_to_check, missions_complete, locations_done, locations):
|
||||
def mission_reqs_completed(ctx: SC2Context, mission_name: str, missions_complete):
|
||||
"""Returns a bool signifying if the mission has all requirements complete and can be done
|
||||
|
||||
Keyword arguments:
|
||||
Arguments:
|
||||
ctx -- instance of SC2Context
|
||||
locations_to_check -- the mission string name to check
|
||||
missions_complete -- an int of how many missions have been completed
|
||||
locations_done -- a list of the location ids that have been complete
|
||||
locations -- a dict of MissionInfo for mission requirements for this world"""
|
||||
if len(locations[location_to_check].required_world) >= 1:
|
||||
"""
|
||||
if len(ctx.mission_req_table[mission_name].required_world) >= 1:
|
||||
# A check for when the requirements are being or'd
|
||||
or_success = False
|
||||
|
||||
# Loop through required missions
|
||||
for req_mission in locations[location_to_check].required_world:
|
||||
for req_mission in ctx.mission_req_table[mission_name].required_world:
|
||||
req_success = True
|
||||
|
||||
# Check if required mission has been completed
|
||||
if not (locations[list(locations)[req_mission-1]].id * 100 + SC2WOL_LOC_ID_OFFSET) in locations_done:
|
||||
if not locations[location_to_check].or_requirements:
|
||||
if not (ctx.mission_req_table[list(ctx.mission_req_table)[req_mission - 1]].id *
|
||||
victory_modulo + SC2WOL_LOC_ID_OFFSET) in ctx.checked_locations:
|
||||
if not ctx.mission_req_table[mission_name].or_requirements:
|
||||
return False
|
||||
else:
|
||||
req_success = False
|
||||
|
||||
# Recursively check required mission to see if it's requirements are met, in case !collect has been done
|
||||
if not mission_reqs_completed(list(locations)[req_mission-1], missions_complete, locations_done,
|
||||
locations):
|
||||
if not locations[location_to_check].or_requirements:
|
||||
if not mission_reqs_completed(ctx, list(ctx.mission_req_table)[req_mission - 1], missions_complete):
|
||||
if not ctx.mission_req_table[mission_name].or_requirements:
|
||||
return False
|
||||
else:
|
||||
req_success = False
|
||||
|
||||
# If requirement check succeeded mark or as satisfied
|
||||
if locations[location_to_check].or_requirements and req_success:
|
||||
if ctx.mission_req_table[mission_name].or_requirements and req_success:
|
||||
or_success = True
|
||||
|
||||
if locations[location_to_check].or_requirements:
|
||||
if ctx.mission_req_table[mission_name].or_requirements:
|
||||
# Return false if or requirements not met
|
||||
if not or_success:
|
||||
return False
|
||||
|
||||
# Check number of missions
|
||||
if missions_complete >= locations[location_to_check].number:
|
||||
if missions_complete >= ctx.mission_req_table[mission_name].number:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
@@ -929,7 +844,7 @@ class DllDirectory:
|
||||
self.set(self._old)
|
||||
|
||||
@staticmethod
|
||||
def get() -> str:
|
||||
def get() -> typing.Optional[str]:
|
||||
if sys.platform == "win32":
|
||||
n = ctypes.windll.kernel32.GetDllDirectoryW(0, None)
|
||||
buf = ctypes.create_unicode_buffer(n)
|
||||
|
||||
2
Utils.py
2
Utils.py
@@ -35,7 +35,7 @@ class Version(typing.NamedTuple):
|
||||
build: int
|
||||
|
||||
|
||||
__version__ = "0.3.4"
|
||||
__version__ = "0.3.5"
|
||||
version_tuple = tuplize_version(__version__)
|
||||
|
||||
is_linux = sys.platform.startswith("linux")
|
||||
|
||||
@@ -12,7 +12,7 @@ ModuleUpdate.update()
|
||||
# in case app gets imported by something like gunicorn
|
||||
import Utils
|
||||
|
||||
Utils.local_path.cached_path = os.path.dirname(__file__)
|
||||
Utils.local_path.cached_path = os.path.dirname(__file__) or "." # py3.8 is not abs. remove "." when dropping 3.8
|
||||
|
||||
from WebHostLib import register, app as raw_app
|
||||
from waitress import serve
|
||||
|
||||
@@ -32,9 +32,12 @@ def download_patch(room_id, patch_id):
|
||||
new_zip.writestr("archipelago.json", json.dumps(manifest))
|
||||
else:
|
||||
new_zip.writestr(file.filename, zf.read(file), file.compress_type, 9)
|
||||
|
||||
if "patch_file_ending" in manifest:
|
||||
patch_file_ending = manifest["patch_file_ending"]
|
||||
else:
|
||||
patch_file_ending = AutoPatchRegister.patch_types[patch.game].patch_file_ending
|
||||
fname = f"P{patch.player_id}_{patch.player_name}_{app.jinja_env.filters['suuid'](room_id)}" \
|
||||
f"{AutoPatchRegister.patch_types[patch.game].patch_file_ending}"
|
||||
f"{patch_file_ending}"
|
||||
new_file.seek(0)
|
||||
return send_file(new_file, as_attachment=True, download_name=fname)
|
||||
else:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
flask>=2.1.3
|
||||
flask>=2.2.2
|
||||
pony>=0.7.16
|
||||
waitress>=2.1.1
|
||||
waitress>=2.1.2
|
||||
Flask-Caching>=2.0.1
|
||||
Flask-Compress>=1.12
|
||||
Flask-Limiter>=2.5.0
|
||||
Flask-Limiter>=2.6.2
|
||||
bokeh>=2.4.3
|
||||
|
||||
@@ -97,6 +97,11 @@ local extensionConsumableLookup = {
|
||||
[443] = 0x3F
|
||||
}
|
||||
|
||||
local noOverworldItemsLookup = {
|
||||
[499] = 0x2B,
|
||||
[500] = 0x12,
|
||||
}
|
||||
|
||||
local itemMessages = {}
|
||||
local consumableStacks = nil
|
||||
local prevstate = ""
|
||||
@@ -341,7 +346,7 @@ function processBlock(block)
|
||||
-- This is a key item
|
||||
memoryLocation = memoryLocation - 0x0E0
|
||||
wU8(memoryLocation, 0x01)
|
||||
elseif v >= 0x1E0 then
|
||||
elseif v >= 0x1E0 and v <= 0x1F2 then
|
||||
-- This is a movement item
|
||||
-- Minus Offset (0x100) - movement offset (0xE0)
|
||||
memoryLocation = memoryLocation - 0x1E0
|
||||
@@ -351,7 +356,10 @@ function processBlock(block)
|
||||
else
|
||||
wU8(memoryLocation, 0x01)
|
||||
end
|
||||
|
||||
elseif v >= 0x1F3 and v <= 0x1F4 then
|
||||
-- NoOverworld special items
|
||||
memoryLocation = noOverworldItemsLookup[v]
|
||||
wU8(memoryLocation, 0x01)
|
||||
elseif v >= 0x16C and v <= 0x1AF then
|
||||
-- This is a gold item
|
||||
amountToAdd = goldLookup[v]
|
||||
|
||||
@@ -56,3 +56,8 @@ SNI is required to use SNIClient. If not integrated into the project, it has to
|
||||
You can get the latest SNI release at [SNI Github releases](https://github.com/alttpo/sni/releases).
|
||||
It should be dropped as "SNI" into the root folder of the project. Alternatively, you can point the sni setting in
|
||||
host.yaml at your SNI folder.
|
||||
|
||||
|
||||
## Running tests
|
||||
|
||||
Run `pip install pytest pytest-subtests`, then use your IDE to run tests or run `pytest` from the source folder.
|
||||
|
||||
@@ -274,14 +274,12 @@ Define a property `option_<name> = <number>` per selectable value and
|
||||
`default = <number>` to set the default selection. Aliases can be set by
|
||||
defining a property `alias_<name> = <same number>`.
|
||||
|
||||
One special case where aliases are required is when option name is `yes`, `no`,
|
||||
`on` or `off` because they parse to `True` or `False`:
|
||||
```python
|
||||
option_off = 0
|
||||
option_on = 1
|
||||
option_some = 2
|
||||
alias_false = 0
|
||||
alias_true = 1
|
||||
alias_disabled = 0
|
||||
alias_enabled = 1
|
||||
default = 0
|
||||
```
|
||||
|
||||
|
||||
@@ -3,6 +3,6 @@ websockets>=10.3
|
||||
PyYAML>=6.0
|
||||
jellyfish>=0.9.0
|
||||
jinja2>=3.1.2
|
||||
schema>=0.7.4
|
||||
schema>=0.7.5
|
||||
kivy>=2.1.0
|
||||
bsdiff4>=1.2.2
|
||||
@@ -371,13 +371,13 @@ class TestDistributeItemsRestrictive(unittest.TestCase):
|
||||
|
||||
distribute_items_restrictive(multi_world)
|
||||
|
||||
self.assertEqual(locations[0].item, basic_items[0])
|
||||
self.assertEqual(locations[0].item, basic_items[1])
|
||||
self.assertFalse(locations[0].event)
|
||||
self.assertEqual(locations[1].item, prog_items[0])
|
||||
self.assertTrue(locations[1].event)
|
||||
self.assertEqual(locations[2].item, prog_items[1])
|
||||
self.assertTrue(locations[2].event)
|
||||
self.assertEqual(locations[3].item, basic_items[1])
|
||||
self.assertEqual(locations[3].item, basic_items[0])
|
||||
self.assertFalse(locations[3].event)
|
||||
|
||||
def test_excluded_distribute(self):
|
||||
@@ -500,8 +500,8 @@ class TestDistributeItemsRestrictive(unittest.TestCase):
|
||||
removed_item: list[Item] = []
|
||||
removed_location: list[Location] = []
|
||||
|
||||
def fill_hook(progitempool, nonexcludeditempool, localrestitempool, nonlocalrestitempool, restitempool, fill_locations):
|
||||
removed_item.append(restitempool.pop(0))
|
||||
def fill_hook(progitempool, usefulitempool, filleritempool, fill_locations):
|
||||
removed_item.append(filleritempool.pop(0))
|
||||
removed_location.append(fill_locations.pop(0))
|
||||
|
||||
multi_world.worlds[player1.id].fill_hook = fill_hook
|
||||
|
||||
@@ -52,3 +52,13 @@ class TestIDs(unittest.TestCase):
|
||||
else:
|
||||
for location_id in world_type.location_id_to_name:
|
||||
self.assertGreater(location_id, 0)
|
||||
|
||||
def testDuplicateItemIDs(self):
|
||||
for gamename, world_type in AutoWorldRegister.world_types.items():
|
||||
with self.subTest(game=gamename):
|
||||
self.assertEqual(len(world_type.item_id_to_name), len(world_type.item_name_to_id))
|
||||
|
||||
def testDuplicateLocationIDs(self):
|
||||
for gamename, world_type in AutoWorldRegister.world_types.items():
|
||||
with self.subTest(game=gamename):
|
||||
self.assertEqual(len(world_type.location_id_to_name), len(world_type.location_name_to_id))
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import unittest
|
||||
from worlds.AutoWorld import AutoWorldRegister
|
||||
from . import setup_default_world
|
||||
|
||||
|
||||
class TestBase(unittest.TestCase):
|
||||
@@ -29,3 +30,17 @@ class TestBase(unittest.TestCase):
|
||||
with self.subTest(group_name, group_name=group_name):
|
||||
for item in items:
|
||||
self.assertIn(item, world_type.item_name_to_id)
|
||||
|
||||
def testItemCountGreaterEqualLocations(self):
|
||||
for game_name, world_type in AutoWorldRegister.world_types.items():
|
||||
|
||||
if game_name in {"Final Fantasy"}:
|
||||
continue
|
||||
with self.subTest("Game", game=game_name):
|
||||
world = setup_default_world(world_type)
|
||||
location_count = sum(0 if location.event or location.item else 1 for location in world.get_locations())
|
||||
self.assertGreaterEqual(
|
||||
len(world.itempool),
|
||||
location_count,
|
||||
f"{game_name} Item count MUST meet or exceede the number of locations",
|
||||
)
|
||||
|
||||
@@ -14,9 +14,20 @@ class TestFileGeneration(unittest.TestCase):
|
||||
|
||||
def testOptions(self):
|
||||
WebHost.create_options_files()
|
||||
self.assertTrue(os.path.exists(os.path.join(self.correct_path, "static", "generated", "configs")))
|
||||
target = os.path.join(self.correct_path, "static", "generated", "configs")
|
||||
self.assertTrue(os.path.exists(target))
|
||||
self.assertFalse(os.path.exists(os.path.join(self.incorrect_path, "static", "generated", "configs")))
|
||||
|
||||
# folder seems fine, so now we try to generate Options based on the default file
|
||||
from WebHostLib.check import roll_options
|
||||
file: os.DirEntry
|
||||
for file in os.scandir(target):
|
||||
if file.is_file() and file.name.endswith(".yaml"):
|
||||
with self.subTest(file=file.name):
|
||||
with open(file) as f:
|
||||
for value in roll_options({file.name: f.read()})[0].values():
|
||||
self.assertTrue(value is True, f"Default Options for template {file.name} cannot be run.")
|
||||
|
||||
def testTutorial(self):
|
||||
WebHost.create_ordered_tutorials_file()
|
||||
self.assertTrue(os.path.exists(os.path.join(self.correct_path, "static", "generated", "tutorials.json")))
|
||||
|
||||
@@ -221,10 +221,8 @@ class World(metaclass=AutoWorldRegister):
|
||||
@classmethod
|
||||
def fill_hook(cls,
|
||||
progitempool: List["Item"],
|
||||
nonexcludeditempool: List["Item"],
|
||||
localrestitempool: Dict[int, List["Item"]],
|
||||
nonlocalrestitempool: Dict[int, List["Item"]],
|
||||
restitempool: List["Item"],
|
||||
usefulitempool: List["Item"],
|
||||
filleritempool: List["Item"],
|
||||
fill_locations: List["Location"]) -> None:
|
||||
"""Special method that gets called as part of distribute_items_restrictive (main fill).
|
||||
This gets called once per present world type."""
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import logging
|
||||
from typing import Optional
|
||||
from typing import Optional, Union, List, Tuple, Callable, Dict
|
||||
|
||||
from BaseClasses import Boss
|
||||
from Fill import FillError
|
||||
from .Options import Bosses
|
||||
|
||||
|
||||
def BossFactory(boss: str, player: int) -> Optional[Boss]:
|
||||
@@ -12,7 +13,7 @@ def BossFactory(boss: str, player: int) -> Optional[Boss]:
|
||||
raise Exception('Unknown Boss: %s', boss)
|
||||
|
||||
|
||||
def ArmosKnightsDefeatRule(state, player: int):
|
||||
def ArmosKnightsDefeatRule(state, player: int) -> bool:
|
||||
# Magic amounts are probably a bit overkill
|
||||
return (
|
||||
state.has_melee_weapon(player) or
|
||||
@@ -25,7 +26,7 @@ def ArmosKnightsDefeatRule(state, player: int):
|
||||
state.has('Red Boomerang', player))
|
||||
|
||||
|
||||
def LanmolasDefeatRule(state, player: int):
|
||||
def LanmolasDefeatRule(state, player: int) -> bool:
|
||||
return (
|
||||
state.has_melee_weapon(player) or
|
||||
state.has('Fire Rod', player) or
|
||||
@@ -35,16 +36,16 @@ def LanmolasDefeatRule(state, player: int):
|
||||
state.can_shoot_arrows(player))
|
||||
|
||||
|
||||
def MoldormDefeatRule(state, player: int):
|
||||
def MoldormDefeatRule(state, player: int) -> bool:
|
||||
return state.has_melee_weapon(player)
|
||||
|
||||
|
||||
def HelmasaurKingDefeatRule(state, player: int):
|
||||
def HelmasaurKingDefeatRule(state, player: int) -> bool:
|
||||
# TODO: technically possible with the hammer
|
||||
return state.has_sword(player) or state.can_shoot_arrows(player)
|
||||
|
||||
|
||||
def ArrghusDefeatRule(state, player: int):
|
||||
def ArrghusDefeatRule(state, player: int) -> bool:
|
||||
if not state.has('Hookshot', player):
|
||||
return False
|
||||
# TODO: ideally we would have a check for bow and silvers, which combined with the
|
||||
@@ -58,7 +59,7 @@ def ArrghusDefeatRule(state, player: int):
|
||||
(state.has('Ice Rod', player) and (state.can_shoot_arrows(player) or state.can_extend_magic(player, 16))))
|
||||
|
||||
|
||||
def MothulaDefeatRule(state, player: int):
|
||||
def MothulaDefeatRule(state, player: int) -> bool:
|
||||
return (
|
||||
state.has_melee_weapon(player) or
|
||||
(state.has('Fire Rod', player) and state.can_extend_magic(player, 10)) or
|
||||
@@ -70,11 +71,11 @@ def MothulaDefeatRule(state, player: int):
|
||||
)
|
||||
|
||||
|
||||
def BlindDefeatRule(state, player: int):
|
||||
def BlindDefeatRule(state, player: int) -> bool:
|
||||
return state.has_melee_weapon(player) or state.has('Cane of Somaria', player) or state.has('Cane of Byrna', player)
|
||||
|
||||
|
||||
def KholdstareDefeatRule(state, player: int):
|
||||
def KholdstareDefeatRule(state, player: int) -> bool:
|
||||
return (
|
||||
(
|
||||
state.has('Fire Rod', player) or
|
||||
@@ -96,11 +97,11 @@ def KholdstareDefeatRule(state, player: int):
|
||||
)
|
||||
|
||||
|
||||
def VitreousDefeatRule(state, player: int):
|
||||
def VitreousDefeatRule(state, player: int) -> bool:
|
||||
return state.can_shoot_arrows(player) or state.has_melee_weapon(player)
|
||||
|
||||
|
||||
def TrinexxDefeatRule(state, player: int):
|
||||
def TrinexxDefeatRule(state, player: int) -> bool:
|
||||
if not (state.has('Fire Rod', player) and state.has('Ice Rod', player)):
|
||||
return False
|
||||
return state.has('Hammer', player) or state.has('Tempered Sword', player) or state.has('Golden Sword', player) or \
|
||||
@@ -108,11 +109,11 @@ def TrinexxDefeatRule(state, player: int):
|
||||
(state.has_sword(player) and state.can_extend_magic(player, 32))
|
||||
|
||||
|
||||
def AgahnimDefeatRule(state, player: int):
|
||||
def AgahnimDefeatRule(state, player: int) -> bool:
|
||||
return state.has_sword(player) or state.has('Hammer', player) or state.has('Bug Catching Net', player)
|
||||
|
||||
|
||||
def GanonDefeatRule(state, player: int):
|
||||
def GanonDefeatRule(state, player: int) -> bool:
|
||||
if state.world.swordless[player]:
|
||||
return state.has('Hammer', player) and \
|
||||
state.has_fire_source(player) and \
|
||||
@@ -132,7 +133,7 @@ def GanonDefeatRule(state, player: int):
|
||||
return common and state.has('Silver Bow', player) and state.can_shoot_arrows(player)
|
||||
|
||||
|
||||
boss_table = {
|
||||
boss_table: Dict[str, Tuple[str, Optional[Callable]]] = {
|
||||
'Armos Knights': ('Armos', ArmosKnightsDefeatRule),
|
||||
'Lanmolas': ('Lanmola', LanmolasDefeatRule),
|
||||
'Moldorm': ('Moldorm', MoldormDefeatRule),
|
||||
@@ -147,7 +148,7 @@ boss_table = {
|
||||
'Agahnim2': ('Agahnim2', AgahnimDefeatRule)
|
||||
}
|
||||
|
||||
boss_location_table = [
|
||||
boss_location_table: List[Tuple[str, str]] = [
|
||||
('Ganons Tower', 'top'),
|
||||
('Tower of Hera', None),
|
||||
('Skull Woods', None),
|
||||
@@ -164,6 +165,34 @@ boss_location_table = [
|
||||
]
|
||||
|
||||
|
||||
def place_plando_bosses(bosses: List[str], world, player: int) -> Tuple[List[str], List[Tuple[str, str]]]:
|
||||
# Most to least restrictive order
|
||||
boss_locations = boss_location_table.copy()
|
||||
world.random.shuffle(boss_locations)
|
||||
boss_locations.sort(key=lambda location: -int(restrictive_boss_locations[location]))
|
||||
already_placed_bosses: List[str] = []
|
||||
|
||||
for boss in bosses:
|
||||
if "-" in boss: # handle plando locations
|
||||
loc, boss = boss.split("-")
|
||||
boss = boss.title()
|
||||
level: str = None
|
||||
if loc.split(" ")[-1] in {"top", "middle", "bottom"}:
|
||||
# split off level
|
||||
loc = loc.split(" ")
|
||||
level = loc[-1]
|
||||
loc = " ".join(loc[:-1])
|
||||
loc = loc.title().replace("Of", "of")
|
||||
place_boss(world, player, boss, loc, level)
|
||||
already_placed_bosses.append(boss)
|
||||
boss_locations.remove((loc, level))
|
||||
else: # boss chosen with no specified locations
|
||||
boss = boss.title()
|
||||
boss_locations, already_placed_bosses = place_where_possible(world, player, boss, boss_locations)
|
||||
|
||||
return already_placed_bosses, boss_locations
|
||||
|
||||
|
||||
def can_place_boss(boss: str, dungeon_name: str, level: Optional[str] = None) -> bool:
|
||||
# blacklist approach
|
||||
if boss in {"Agahnim", "Agahnim2", "Ganon"}:
|
||||
@@ -187,62 +216,50 @@ def can_place_boss(boss: str, dungeon_name: str, level: Optional[str] = None) ->
|
||||
|
||||
return True
|
||||
|
||||
restrictive_boss_locations = {}
|
||||
|
||||
restrictive_boss_locations: Dict[Tuple[str, str], bool] = {}
|
||||
for location in boss_location_table:
|
||||
restrictive_boss_locations[location] = not all(can_place_boss(boss, *location)
|
||||
for boss in boss_table if not boss.startswith("Agahnim"))
|
||||
|
||||
def place_boss(world, player: int, boss: str, location: str, level: Optional[str]):
|
||||
|
||||
def place_boss(world, player: int, boss: str, location: str, level: Optional[str]) -> None:
|
||||
if location == 'Ganons Tower' and world.mode[player] == 'inverted':
|
||||
location = 'Inverted Ganons Tower'
|
||||
logging.debug('Placing boss %s at %s', boss, location + (' (' + level + ')' if level else ''))
|
||||
world.get_dungeon(location, player).bosses[level] = BossFactory(boss, player)
|
||||
|
||||
def format_boss_location(location, level):
|
||||
|
||||
def format_boss_location(location: str, level: str) -> str:
|
||||
return location + (' (' + level + ')' if level else '')
|
||||
|
||||
def place_bosses(world, player: int):
|
||||
if world.boss_shuffle[player] == 'none':
|
||||
|
||||
def place_bosses(world, player: int) -> None:
|
||||
# will either be an int or a lower case string with ';' between options
|
||||
boss_shuffle: Union[str, int] = world.boss_shuffle[player].value
|
||||
already_placed_bosses: List[str] = []
|
||||
remaining_locations: List[Tuple[str, str]] = []
|
||||
# handle plando
|
||||
if isinstance(boss_shuffle, str):
|
||||
# figure out our remaining mode, convert it to an int and remove it from plando_args
|
||||
options = boss_shuffle.split(";")
|
||||
boss_shuffle = Bosses.options[options.pop()]
|
||||
# place our plando bosses
|
||||
already_placed_bosses, remaining_locations = place_plando_bosses(options, world, player)
|
||||
if boss_shuffle == Bosses.option_none: # vanilla boss locations
|
||||
return
|
||||
|
||||
# Most to least restrictive order
|
||||
boss_locations = boss_location_table.copy()
|
||||
world.random.shuffle(boss_locations)
|
||||
boss_locations.sort(key= lambda location: -int(restrictive_boss_locations[location]))
|
||||
if not remaining_locations and not already_placed_bosses:
|
||||
remaining_locations = boss_location_table.copy()
|
||||
world.random.shuffle(remaining_locations)
|
||||
remaining_locations.sort(key=lambda location: -int(restrictive_boss_locations[location]))
|
||||
|
||||
all_bosses = sorted(boss_table.keys()) # sorted to be deterministic on older pythons
|
||||
placeable_bosses = [boss for boss in all_bosses if boss not in ['Agahnim', 'Agahnim2', 'Ganon']]
|
||||
|
||||
shuffle_mode = world.boss_shuffle[player]
|
||||
already_placed_bosses = []
|
||||
if ";" in shuffle_mode:
|
||||
bosses = shuffle_mode.split(";")
|
||||
shuffle_mode = bosses.pop()
|
||||
for boss in bosses:
|
||||
if "-" in boss:
|
||||
loc, boss = boss.split("-")
|
||||
boss = boss.title()
|
||||
level = None
|
||||
if loc.split(" ")[-1] in {"top", "middle", "bottom"}:
|
||||
# split off level
|
||||
loc = loc.split(" ")
|
||||
level = loc[-1]
|
||||
loc = " ".join(loc[:-1])
|
||||
loc = loc.title().replace("Of", "of")
|
||||
if can_place_boss(boss, loc, level) and (loc, level) in boss_locations:
|
||||
place_boss(world, player, boss, loc, level)
|
||||
already_placed_bosses.append(boss)
|
||||
boss_locations.remove((loc, level))
|
||||
else:
|
||||
raise Exception(f"Cannot place {boss} at {format_boss_location(loc, level)} for player {player}.")
|
||||
else:
|
||||
boss = boss.title()
|
||||
boss_locations, already_placed_bosses = place_where_possible(world, player, boss, boss_locations)
|
||||
|
||||
if shuffle_mode == "none":
|
||||
return # vanilla bosses come pre-placed
|
||||
|
||||
if shuffle_mode in ["basic", "full"]:
|
||||
if world.boss_shuffle[player] == "basic": # vanilla bosses shuffled
|
||||
if boss_shuffle == Bosses.option_basic or boss_shuffle == Bosses.option_full:
|
||||
if boss_shuffle == Bosses.option_basic: # vanilla bosses shuffled
|
||||
bosses = placeable_bosses + ['Armos Knights', 'Lanmolas', 'Moldorm']
|
||||
else: # all bosses present, the three duplicates chosen at random
|
||||
bosses = placeable_bosses + world.random.sample(placeable_bosses, 3)
|
||||
@@ -258,7 +275,7 @@ def place_bosses(world, player: int):
|
||||
logging.debug('Bosses chosen %s', bosses)
|
||||
|
||||
world.random.shuffle(bosses)
|
||||
for loc, level in boss_locations:
|
||||
for loc, level in remaining_locations:
|
||||
for _ in range(len(bosses)):
|
||||
boss = bosses.pop()
|
||||
if can_place_boss(boss, loc, level):
|
||||
@@ -272,8 +289,8 @@ def place_bosses(world, player: int):
|
||||
|
||||
place_boss(world, player, boss, loc, level)
|
||||
|
||||
elif shuffle_mode == "chaos": # all bosses chosen at random
|
||||
for loc, level in boss_locations:
|
||||
elif boss_shuffle == Bosses.option_chaos: # all bosses chosen at random
|
||||
for loc, level in remaining_locations:
|
||||
try:
|
||||
boss = world.random.choice(
|
||||
[b for b in placeable_bosses if can_place_boss(b, loc, level)])
|
||||
@@ -282,9 +299,9 @@ def place_bosses(world, player: int):
|
||||
else:
|
||||
place_boss(world, player, boss, loc, level)
|
||||
|
||||
elif shuffle_mode == "singularity":
|
||||
elif boss_shuffle == Bosses.option_singularity:
|
||||
primary_boss = world.random.choice(placeable_bosses)
|
||||
remaining_boss_locations, _ = place_where_possible(world, player, primary_boss, boss_locations)
|
||||
remaining_boss_locations, _ = place_where_possible(world, player, primary_boss, remaining_locations)
|
||||
if remaining_boss_locations:
|
||||
# pick a boss to go into the remaining locations
|
||||
remaining_boss = world.random.choice([boss for boss in placeable_bosses if all(
|
||||
@@ -293,12 +310,12 @@ def place_bosses(world, player: int):
|
||||
if remaining_boss_locations:
|
||||
raise Exception("Unfilled boss locations!")
|
||||
else:
|
||||
raise FillError(f"Could not find boss shuffle mode {shuffle_mode}")
|
||||
raise FillError(f"Could not find boss shuffle mode {boss_shuffle}")
|
||||
|
||||
|
||||
def place_where_possible(world, player: int, boss: str, boss_locations):
|
||||
remainder = []
|
||||
placed_bosses = []
|
||||
def place_where_possible(world, player: int, boss: str, boss_locations) -> Tuple[List[Tuple[str, str]], List[str]]:
|
||||
remainder: List[Tuple[str, str]] = []
|
||||
placed_bosses: List[str] = []
|
||||
for loc, level in boss_locations:
|
||||
# place that boss where it can go
|
||||
if can_place_boss(boss, loc, level):
|
||||
|
||||
@@ -212,9 +212,7 @@ def parse_arguments(argv, no_defaults=False):
|
||||
Alternatively, can be a ALttP Rom patched with a Link
|
||||
sprite that will be extracted.
|
||||
''')
|
||||
parser.add_argument('--gui', help='Launch the GUI', action='store_true')
|
||||
|
||||
parser.add_argument('--enemizercli', default=defval('EnemizerCLI/EnemizerCLI.Core'))
|
||||
parser.add_argument('--shufflebosses', default=defval('none'), choices=['none', 'basic', 'normal', 'chaos',
|
||||
"singularity"])
|
||||
|
||||
|
||||
@@ -480,7 +480,7 @@ def set_up_take_anys(world, player):
|
||||
old_man_take_any.shop = TakeAny(old_man_take_any, 0x0112, 0xE2, True, True, total_shop_slots)
|
||||
world.shops.append(old_man_take_any.shop)
|
||||
|
||||
swords = [item for item in world.itempool if item.type == 'Sword' and item.player == player]
|
||||
swords = [item for item in world.itempool if item.player == player and item.type == 'Sword']
|
||||
if swords:
|
||||
sword = world.random.choice(swords)
|
||||
world.itempool.remove(sword)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import typing
|
||||
|
||||
from BaseClasses import MultiWorld
|
||||
from Options import Choice, Range, Option, Toggle, DefaultOnToggle, DeathLink
|
||||
from Options import Choice, Range, Option, Toggle, DefaultOnToggle, DeathLink, TextChoice
|
||||
|
||||
|
||||
class Logic(Choice):
|
||||
@@ -39,8 +39,6 @@ class OpenPyramid(Choice):
|
||||
option_auto = 3
|
||||
default = option_goal
|
||||
|
||||
alias_true = option_open
|
||||
alias_false = option_closed
|
||||
alias_yes = option_open
|
||||
alias_no = option_closed
|
||||
|
||||
@@ -140,13 +138,143 @@ class WorldState(Choice):
|
||||
option_inverted = 2
|
||||
|
||||
|
||||
class Bosses(Choice):
|
||||
option_vanilla = 0
|
||||
option_simple = 1
|
||||
class Bosses(TextChoice):
|
||||
"""Shuffles bosses around to different locations.
|
||||
Basic will shuffle all bosses except Ganon and Agahnim anywhere they can be placed.
|
||||
Full chooses 3 bosses at random to be placed twice instead of Lanmolas, Moldorm, and Helmasaur.
|
||||
Chaos allows any boss to appear any number of times.
|
||||
Singularity places a single boss in as many places as possible, and a second boss in any remaining locations.
|
||||
Supports plando placement. Formatting here: https://archipelago.gg/tutorial/A%20Link%20to%20the%20Past/plando/en"""
|
||||
display_name = "Boss Shuffle"
|
||||
option_none = 0
|
||||
option_basic = 1
|
||||
option_full = 2
|
||||
option_chaos = 3
|
||||
option_singularity = 4
|
||||
|
||||
bosses: set = {
|
||||
"Armos Knights",
|
||||
"Lanmolas",
|
||||
"Moldorm",
|
||||
"Helmasaur King",
|
||||
"Arrghus",
|
||||
"Mothula",
|
||||
"Blind",
|
||||
"Kholdstare",
|
||||
"Vitreous",
|
||||
"Trinexx",
|
||||
}
|
||||
|
||||
locations: set = {
|
||||
"Ganons Tower Top",
|
||||
"Tower of Hera",
|
||||
"Skull Woods",
|
||||
"Ganons Tower Middle",
|
||||
"Eastern Palace",
|
||||
"Desert Palace",
|
||||
"Palace of Darkness",
|
||||
"Swamp Palace",
|
||||
"Thieves Town",
|
||||
"Ice Palace",
|
||||
"Misery Mire",
|
||||
"Turtle Rock",
|
||||
"Ganons Tower Bottom"
|
||||
}
|
||||
|
||||
def __init__(self, value: typing.Union[str, int]):
|
||||
assert isinstance(value, str) or isinstance(value, int), \
|
||||
f"{value} is not a valid option for {self.__class__.__name__}"
|
||||
self.value = value
|
||||
|
||||
@classmethod
|
||||
def from_text(cls, text: str):
|
||||
import random
|
||||
# set all of our text to lower case for name checking
|
||||
text = text.lower()
|
||||
cls.bosses = {boss_name.lower() for boss_name in cls.bosses}
|
||||
cls.locations = {boss_location.lower() for boss_location in cls.locations}
|
||||
if text == "random":
|
||||
return cls(random.choice(list(cls.options.values())))
|
||||
for option_name, value in cls.options.items():
|
||||
if option_name == text:
|
||||
return cls(value)
|
||||
options = text.split(";")
|
||||
|
||||
# since plando exists in the option verify the plando values given are valid
|
||||
cls.validate_plando_bosses(options)
|
||||
|
||||
# find out what type of boss shuffle we should use for placing bosses after plando
|
||||
# and add as a string to look nice in the spoiler
|
||||
if "random" in options:
|
||||
shuffle = random.choice(list(cls.options))
|
||||
options.remove("random")
|
||||
options = ";".join(options) + ";" + shuffle
|
||||
boss_class = cls(options)
|
||||
else:
|
||||
for option in options:
|
||||
if option in cls.options:
|
||||
boss_class = cls(";".join(options))
|
||||
break
|
||||
else:
|
||||
if len(options) == 1:
|
||||
if cls.valid_boss_name(options[0]):
|
||||
options = options[0] + ";singularity"
|
||||
boss_class = cls(options)
|
||||
else:
|
||||
options = options[0] + ";none"
|
||||
boss_class = cls(options)
|
||||
else:
|
||||
options = ";".join(options) + ";none"
|
||||
boss_class = cls(options)
|
||||
return boss_class
|
||||
|
||||
@classmethod
|
||||
def validate_plando_bosses(cls, options: typing.List[str]) -> None:
|
||||
from .Bosses import can_place_boss, format_boss_location
|
||||
for option in options:
|
||||
if option == "random" or option in cls.options:
|
||||
if option != options[-1]:
|
||||
raise ValueError(f"{option} option must be at the end of the boss_shuffle options!")
|
||||
continue
|
||||
if "-" in option:
|
||||
location, boss = option.split("-")
|
||||
level = ''
|
||||
if not cls.valid_boss_name(boss):
|
||||
raise ValueError(f"{boss} is not a valid boss name for location {location}.")
|
||||
if not cls.valid_location_name(location):
|
||||
raise ValueError(f"{location} is not a valid boss location name.")
|
||||
if location.split(" ")[-1] in ("top", "middle", "bottom"):
|
||||
location = location.split(" ")
|
||||
level = location[-1]
|
||||
location = " ".join(location[:-1])
|
||||
location = location.title().replace("Of", "of")
|
||||
if not can_place_boss(boss.title(), location, level):
|
||||
raise ValueError(f"{format_boss_location(location, level)} "
|
||||
f"is not a valid location for {boss.title()}.")
|
||||
else:
|
||||
if not cls.valid_boss_name(option):
|
||||
raise ValueError(f"{option} is not a valid boss name.")
|
||||
|
||||
@classmethod
|
||||
def valid_boss_name(cls, value: str) -> bool:
|
||||
return value.lower() in cls.bosses
|
||||
|
||||
@classmethod
|
||||
def valid_location_name(cls, value: str) -> bool:
|
||||
return value in cls.locations
|
||||
|
||||
def verify(self, world, player_name: str, plando_options) -> None:
|
||||
if isinstance(self.value, int):
|
||||
return
|
||||
from Generate import PlandoSettings
|
||||
if not(PlandoSettings.bosses & plando_options):
|
||||
import logging
|
||||
# plando is disabled but plando options were given so pull the option and change it to an int
|
||||
option = self.value.split(";")[-1]
|
||||
self.value = self.options[option]
|
||||
logging.warning(f"The plando bosses module is turned off, so {self.name_lookup[self.value].title()} "
|
||||
f"boss shuffle will be used for player {player_name}.")
|
||||
|
||||
|
||||
class Enemies(Choice):
|
||||
option_vanilla = 0
|
||||
@@ -159,8 +287,6 @@ class Progressive(Choice):
|
||||
option_off = 0
|
||||
option_grouped_random = 1
|
||||
option_on = 2
|
||||
alias_false = 0
|
||||
alias_true = 2
|
||||
default = 2
|
||||
|
||||
def want_progressives(self, random):
|
||||
@@ -168,8 +294,8 @@ class Progressive(Choice):
|
||||
|
||||
|
||||
class Swordless(Toggle):
|
||||
"""No swords. Curtains in Skull Woods and Agahnim\'s
|
||||
Tower are removed, Agahnim\'s Tower barrier can be
|
||||
"""No swords. Curtains in Skull Woods and Agahnim's
|
||||
Tower are removed, Agahnim's Tower barrier can be
|
||||
destroyed with hammer. Misery Mire and Turtle Rock
|
||||
can be opened without a sword. Hammer damages Ganon.
|
||||
Ether and Bombos Tablet can be activated with Hammer
|
||||
@@ -202,8 +328,6 @@ class Hints(Choice):
|
||||
option_on = 2
|
||||
option_full = 3
|
||||
default = 2
|
||||
alias_false = 0
|
||||
alias_true = 2
|
||||
|
||||
|
||||
class Scams(Choice):
|
||||
@@ -213,7 +337,6 @@ class Scams(Choice):
|
||||
option_king_zora = 1
|
||||
option_bottle_merchant = 2
|
||||
option_all = 3
|
||||
alias_false = 0
|
||||
|
||||
@property
|
||||
def gives_king_zora_hint(self):
|
||||
@@ -282,8 +405,8 @@ class ShieldPalette(Palette):
|
||||
display_name = "Shield Palette"
|
||||
|
||||
|
||||
class LinkPalette(Palette):
|
||||
display_name = "Link Palette"
|
||||
# class LinkPalette(Palette):
|
||||
# display_name = "Link Palette"
|
||||
|
||||
|
||||
class HeartBeep(Choice):
|
||||
@@ -293,7 +416,6 @@ class HeartBeep(Choice):
|
||||
option_half = 2
|
||||
option_quarter = 3
|
||||
option_off = 4
|
||||
alias_false = 4
|
||||
|
||||
|
||||
class HeartColor(Choice):
|
||||
@@ -375,6 +497,7 @@ alttp_options: typing.Dict[str, type(Option)] = {
|
||||
"hints": Hints,
|
||||
"scams": Scams,
|
||||
"restrict_dungeon_item_on_boss": RestrictBossItem,
|
||||
"boss_shuffle": Bosses,
|
||||
"pot_shuffle": PotShuffle,
|
||||
"enemy_shuffle": EnemyShuffle,
|
||||
"killable_thieves": KillableThieves,
|
||||
@@ -387,7 +510,7 @@ alttp_options: typing.Dict[str, type(Option)] = {
|
||||
"hud_palettes": HUDPalette,
|
||||
"sword_palettes": SwordPalette,
|
||||
"shield_palettes": ShieldPalette,
|
||||
"link_palettes": LinkPalette,
|
||||
# "link_palettes": LinkPalette,
|
||||
"heartbeep": HeartBeep,
|
||||
"heartcolor": HeartColor,
|
||||
"quickswap": QuickSwap,
|
||||
|
||||
@@ -34,7 +34,7 @@ from worlds.alttp.Text import KingsReturn_texts, Sanctuary_texts, Kakariko_texts
|
||||
DeathMountain_texts, \
|
||||
LostWoods_texts, WishingWell_texts, DesertPalace_texts, MountainTower_texts, LinksHouse_texts, Lumberjacks_texts, \
|
||||
SickKid_texts, FluteBoy_texts, Zora_texts, MagicShop_texts, Sahasrahla_names
|
||||
from Utils import local_path, user_path, int16_as_bytes, int32_as_bytes, snes_to_pc, is_frozen
|
||||
from Utils import local_path, user_path, int16_as_bytes, int32_as_bytes, snes_to_pc, is_frozen, parse_yaml
|
||||
from worlds.alttp.Items import ItemFactory, item_table, item_name_groups, progression_items
|
||||
from worlds.alttp.EntranceShuffle import door_addresses
|
||||
from worlds.alttp.Options import smallkey_shuffle
|
||||
@@ -551,18 +551,22 @@ class Sprite():
|
||||
Sprite.base_data = Sprite.sprite + Sprite.palette + Sprite.glove_palette
|
||||
|
||||
def from_ap_sprite(self, filedata):
|
||||
filedata = filedata.decode("utf-8-sig")
|
||||
import yaml
|
||||
obj = yaml.safe_load(filedata)
|
||||
if obj["min_format_version"] > 1:
|
||||
raise Exception("Sprite file requires an updated reader.")
|
||||
self.author_name = obj["author"]
|
||||
self.name = obj["name"]
|
||||
if obj["data"]: # skip patching for vanilla content
|
||||
data = bsdiff4.patch(Sprite.base_data, obj["data"])
|
||||
self.sprite = data[:self.sprite_size]
|
||||
self.palette = data[self.sprite_size:self.palette_size]
|
||||
self.glove_palette = data[self.sprite_size + self.palette_size:]
|
||||
# noinspection PyBroadException
|
||||
try:
|
||||
obj = parse_yaml(filedata.decode("utf-8-sig"))
|
||||
if obj["min_format_version"] > 1:
|
||||
raise Exception("Sprite file requires an updated reader.")
|
||||
self.author_name = obj["author"]
|
||||
self.name = obj["name"]
|
||||
if obj["data"]: # skip patching for vanilla content
|
||||
data = bsdiff4.patch(Sprite.base_data, obj["data"])
|
||||
self.sprite = data[:self.sprite_size]
|
||||
self.palette = data[self.sprite_size:self.palette_size]
|
||||
self.glove_palette = data[self.sprite_size + self.palette_size:]
|
||||
except Exception:
|
||||
logger = logging.getLogger("apsprite")
|
||||
logger.exception("Error parsing apsprite file")
|
||||
self.valid = False
|
||||
|
||||
@property
|
||||
def author_game_display(self) -> str:
|
||||
@@ -659,7 +663,7 @@ class Sprite():
|
||||
|
||||
@staticmethod
|
||||
def parse_zspr(filedata, expected_kind):
|
||||
logger = logging.getLogger('ZSPR')
|
||||
logger = logging.getLogger("ZSPR")
|
||||
headerstr = "<4xBHHIHIHH6x"
|
||||
headersize = struct.calcsize(headerstr)
|
||||
if len(filedata) < headersize:
|
||||
@@ -667,7 +671,7 @@ class Sprite():
|
||||
version, csum, icsum, sprite_offset, sprite_size, palette_offset, palette_size, kind = struct.unpack_from(
|
||||
headerstr, filedata)
|
||||
if version not in [1]:
|
||||
logger.error('Error parsing ZSPR file: Version %g not supported', version)
|
||||
logger.error("Error parsing ZSPR file: Version %g not supported", version)
|
||||
return None
|
||||
if kind != expected_kind:
|
||||
return None
|
||||
@@ -676,36 +680,42 @@ class Sprite():
|
||||
stream.seek(headersize)
|
||||
|
||||
def read_utf16le(stream):
|
||||
"Decodes a null-terminated UTF-16_LE string of unknown size from a stream"
|
||||
"""Decodes a null-terminated UTF-16_LE string of unknown size from a stream"""
|
||||
raw = bytearray()
|
||||
while True:
|
||||
char = stream.read(2)
|
||||
if char in [b'', b'\x00\x00']:
|
||||
if char in [b"", b"\x00\x00"]:
|
||||
break
|
||||
raw += char
|
||||
return raw.decode('utf-16_le')
|
||||
return raw.decode("utf-16_le")
|
||||
|
||||
sprite_name = read_utf16le(stream)
|
||||
author_name = read_utf16le(stream)
|
||||
author_credits_name = stream.read().split(b"\x00", 1)[0].decode()
|
||||
# noinspection PyBroadException
|
||||
try:
|
||||
sprite_name = read_utf16le(stream)
|
||||
author_name = read_utf16le(stream)
|
||||
author_credits_name = stream.read().split(b"\x00", 1)[0].decode()
|
||||
|
||||
# Ignoring the Author Rom name for the time being.
|
||||
# Ignoring the Author Rom name for the time being.
|
||||
|
||||
real_csum = sum(filedata) % 0x10000
|
||||
if real_csum != csum or real_csum ^ 0xFFFF != icsum:
|
||||
logger.warning('ZSPR file has incorrect checksum. It may be corrupted.')
|
||||
real_csum = sum(filedata) % 0x10000
|
||||
if real_csum != csum or real_csum ^ 0xFFFF != icsum:
|
||||
logger.warning("ZSPR file has incorrect checksum. It may be corrupted.")
|
||||
|
||||
sprite = filedata[sprite_offset:sprite_offset + sprite_size]
|
||||
palette = filedata[palette_offset:palette_offset + palette_size]
|
||||
sprite = filedata[sprite_offset:sprite_offset + sprite_size]
|
||||
palette = filedata[palette_offset:palette_offset + palette_size]
|
||||
|
||||
if len(sprite) != sprite_size or len(palette) != palette_size:
|
||||
logger.error('Error parsing ZSPR file: Unexpected end of file')
|
||||
if len(sprite) != sprite_size or len(palette) != palette_size:
|
||||
logger.error("Error parsing ZSPR file: Unexpected end of file")
|
||||
return None
|
||||
|
||||
return sprite, palette, sprite_name, author_name, author_credits_name
|
||||
|
||||
except Exception:
|
||||
logger.exception("Error parsing ZSPR file")
|
||||
return None
|
||||
|
||||
return (sprite, palette, sprite_name, author_name, author_credits_name)
|
||||
|
||||
def decode_palette(self):
|
||||
"Returns the palettes as an array of arrays of 15 colors"
|
||||
"""Returns the palettes as an array of arrays of 15 colors"""
|
||||
|
||||
def array_chunk(arr, size):
|
||||
return list(zip(*[iter(arr)] * size))
|
||||
|
||||
@@ -4,6 +4,7 @@ import random
|
||||
import threading
|
||||
import typing
|
||||
|
||||
import Utils
|
||||
from BaseClasses import Item, CollectionState, Tutorial
|
||||
from .Dungeons import create_dungeons
|
||||
from .EntranceShuffle import link_entrances, link_inverted_entrances, plando_connect
|
||||
@@ -136,6 +137,10 @@ class ALTTPWorld(World):
|
||||
|
||||
create_items = generate_itempool
|
||||
|
||||
enemizer_path: str = Utils.get_options()["generator"]["enemizer_path"] \
|
||||
if os.path.isabs(Utils.get_options()["generator"]["enemizer_path"]) \
|
||||
else Utils.local_path(Utils.get_options()["generator"]["enemizer_path"])
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.dungeon_local_item_names = set()
|
||||
self.dungeon_specific_item_names = set()
|
||||
@@ -150,12 +155,12 @@ class ALTTPWorld(World):
|
||||
raise FileNotFoundError(rom_file)
|
||||
|
||||
def generate_early(self):
|
||||
if self.use_enemizer():
|
||||
check_enemizer(self.enemizer_path)
|
||||
|
||||
player = self.player
|
||||
world = self.world
|
||||
|
||||
if self.use_enemizer():
|
||||
check_enemizer(world.enemizer)
|
||||
|
||||
# system for sharing ER layouts
|
||||
self.er_seed = str(world.random.randint(0, 2 ** 64))
|
||||
|
||||
@@ -344,7 +349,7 @@ class ALTTPWorld(World):
|
||||
def use_enemizer(self):
|
||||
world = self.world
|
||||
player = self.player
|
||||
return (world.boss_shuffle[player] != 'none' or world.enemy_shuffle[player]
|
||||
return (world.boss_shuffle[player] or world.enemy_shuffle[player]
|
||||
or world.enemy_health[player] != 'default' or world.enemy_damage[player] != 'default'
|
||||
or world.pot_shuffle[player] or world.bush_shuffle[player]
|
||||
or world.killable_thieves[player])
|
||||
@@ -360,7 +365,7 @@ class ALTTPWorld(World):
|
||||
patch_rom(world, rom, player, use_enemizer)
|
||||
|
||||
if use_enemizer:
|
||||
patch_enemizer(world, player, rom, world.enemizer, output_directory)
|
||||
patch_enemizer(world, player, rom, self.enemizer_path, output_directory)
|
||||
|
||||
if world.is_race:
|
||||
patch_race_rom(rom, world, player)
|
||||
@@ -373,7 +378,7 @@ class ALTTPWorld(World):
|
||||
'hud': world.hud_palettes[player],
|
||||
'sword': world.sword_palettes[player],
|
||||
'shield': world.shield_palettes[player],
|
||||
'link': world.link_palettes[player]
|
||||
# 'link': world.link_palettes[player]
|
||||
}
|
||||
palettes_options = {key: option.current_key for key, option in palettes_options.items()}
|
||||
|
||||
@@ -419,8 +424,7 @@ class ALTTPWorld(World):
|
||||
return ALttPItem(name, self.player, **item_init_table[name])
|
||||
|
||||
@classmethod
|
||||
def stage_fill_hook(cls, world, progitempool, nonexcludeditempool, localrestitempool, nonlocalrestitempool,
|
||||
restitempool, fill_locations):
|
||||
def stage_fill_hook(cls, world, progitempool, usefulitempool, filleritempool, fill_locations):
|
||||
trash_counts = {}
|
||||
standard_keyshuffle_players = set()
|
||||
for player in world.get_game_players("A Link to the Past"):
|
||||
@@ -467,26 +471,15 @@ class ALTTPWorld(World):
|
||||
for player, trash_count in trash_counts.items():
|
||||
gtower_locations = locations_mapping[player]
|
||||
world.random.shuffle(gtower_locations)
|
||||
localrest = localrestitempool[player]
|
||||
if localrest:
|
||||
gt_item_pool = restitempool + localrest
|
||||
world.random.shuffle(gt_item_pool)
|
||||
else:
|
||||
gt_item_pool = restitempool.copy()
|
||||
|
||||
while gtower_locations and gt_item_pool and trash_count > 0:
|
||||
while gtower_locations and filleritempool and trash_count > 0:
|
||||
spot_to_fill = gtower_locations.pop()
|
||||
item_to_place = gt_item_pool.pop()
|
||||
item_to_place = filleritempool.pop()
|
||||
if spot_to_fill.item_rule(item_to_place):
|
||||
if item_to_place in localrest:
|
||||
localrest.remove(item_to_place)
|
||||
else:
|
||||
restitempool.remove(item_to_place)
|
||||
world.push_item(spot_to_fill, item_to_place, False)
|
||||
fill_locations.remove(spot_to_fill) # very slow, unfortunately
|
||||
trash_count -= 1
|
||||
|
||||
|
||||
def get_filler_item_name(self) -> str:
|
||||
if self.world.goal[self.player] == "icerodhunt":
|
||||
item = "Nothing"
|
||||
|
||||
@@ -26,10 +26,14 @@
|
||||
- Example: `Trinexx`
|
||||
- Takes a particular boss and places that boss in any remaining slots in which this boss can function.
|
||||
- In this example, it would fill Desert Palace, but not Tower of Hera.
|
||||
- If no other options are provided this will follow normal singularity rules with that boss.
|
||||
- Boss Shuffle:
|
||||
- Example: `simple`
|
||||
- Example: `basic`
|
||||
- Runs a particular boss shuffle mode to finish construction instead of vanilla placement, typically used as
|
||||
a last instruction.
|
||||
- Supports `random` which will choose a random option from the normal choices.
|
||||
- If one is not supplied any remaining locations will be unshuffled unless a single specific boss is
|
||||
supplied in which case it will use singularity as noted above.
|
||||
- [Available Bosses](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/alttp/Bosses.py#L135)
|
||||
- [Available Arenas](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/alttp/Bosses.py#L150)
|
||||
|
||||
|
||||
@@ -502,6 +502,10 @@ def patch_rom(world, rom, player, active_level_list):
|
||||
# Make Swanky free
|
||||
rom.write_byte(0x348C48, 0x00)
|
||||
|
||||
rom.write_bytes(0x34AB70, bytearray([0xEA, 0xEA]))
|
||||
rom.write_bytes(0x34ABF7, bytearray([0xEA, 0xEA]))
|
||||
rom.write_bytes(0x34ACD0, bytearray([0xEA, 0xEA]))
|
||||
|
||||
# Banana Bird Costs
|
||||
if world.goal[player] == "banana_bird_hunt":
|
||||
banana_bird_cost = math.floor(world.number_of_banana_birds[player] * world.percentage_of_banana_birds[player] / 100.0)
|
||||
|
||||
@@ -137,8 +137,6 @@ class Progressive(Choice):
|
||||
option_off = 0
|
||||
option_grouped_random = 1
|
||||
option_on = 2
|
||||
alias_false = 0
|
||||
alias_true = 2
|
||||
default = 2
|
||||
|
||||
def want_progressives(self, random):
|
||||
|
||||
@@ -249,6 +249,10 @@ script.on_event(defines.events.on_player_main_inventory_changed, update_player_e
|
||||
|
||||
function add_samples(force, name, count)
|
||||
local function add_to_table(t)
|
||||
if count <= 0 then
|
||||
-- Fixes a bug with single craft, if a recipe gives 0 of a given item.
|
||||
return
|
||||
end
|
||||
t[name] = (t[name] or 0) + count
|
||||
end
|
||||
-- Add to global table of earned samples for future new players
|
||||
|
||||
@@ -20,7 +20,7 @@ FF1_STARTER_ITEMS = [
|
||||
|
||||
FF1_PROGRESSION_LIST = [
|
||||
"Rod", "Cube", "Lute", "Key", "Chime", "Oxyale",
|
||||
"Ship", "Canoe", "Floater", "Canal",
|
||||
"Ship", "Canoe", "Floater", "Mark", "Sigil", "Canal",
|
||||
"Crown", "Crystal", "Herb", "Tnt", "Adamant", "Slab", "Ruby", "Bottle",
|
||||
"Shard",
|
||||
"EarthOrb", "FireOrb", "WaterOrb", "AirOrb"
|
||||
|
||||
@@ -31,7 +31,7 @@ class FF1World(World):
|
||||
game = "Final Fantasy"
|
||||
topology_present = False
|
||||
remote_items = True
|
||||
data_version = 1
|
||||
data_version = 2
|
||||
remote_start_inventory = True
|
||||
|
||||
ff1_items = FF1Items()
|
||||
@@ -66,7 +66,10 @@ class FF1World(World):
|
||||
def goal_rule_and_shards(state):
|
||||
return goal_rule(state) and state.has("Shard", self.player, 32)
|
||||
terminated_event.access_rule = goal_rule_and_shards
|
||||
|
||||
if "MARK" in items.keys():
|
||||
# Fail generation for Noverworld and provide link to old FFR website
|
||||
raise Exception("FFR Noverworld seeds must be generated on an older version of FFR. Please ensure you generated the settings using "
|
||||
"4-4-0.finalfantasyrandomizer.com")
|
||||
menu_region.locations.append(terminated_event)
|
||||
self.world.regions += [menu_region]
|
||||
|
||||
|
||||
@@ -190,5 +190,7 @@
|
||||
"Ship": 480,
|
||||
"Bridge": 488,
|
||||
"Canal": 492,
|
||||
"Canoe": 498
|
||||
"Canoe": 498,
|
||||
"Sigil": 499,
|
||||
"Mark": 500
|
||||
}
|
||||
|
||||
@@ -409,7 +409,6 @@ class DeathLink(Choice):
|
||||
shade: DeathLink functions like a normal death if you do not already have a shade, shadeless otherwise.
|
||||
"""
|
||||
option_off = 0
|
||||
alias_false = 0
|
||||
alias_no = 0
|
||||
alias_true = 1
|
||||
alias_on = 1
|
||||
@@ -435,10 +434,8 @@ class CostSanity(Choice):
|
||||
These costs can be in Geo (except Grubfather, Seer and Eggshop), Grubs, Charms, Essence and/or Rancid Eggs
|
||||
"""
|
||||
option_off = 0
|
||||
alias_false = 0
|
||||
alias_no = 0
|
||||
option_on = 1
|
||||
alias_true = 1
|
||||
alias_yes = 1
|
||||
option_shopsonly = 2
|
||||
option_notshops = 3
|
||||
|
||||
@@ -7,9 +7,9 @@ config file.
|
||||
|
||||
## What does randomization do to this game?
|
||||
|
||||
Recipes are removed from the crafting book and shuffled into the item pool. It can also optionally change which
|
||||
Some recipes are locked from being able to be crafted and shuffled into the item pool. It can also optionally change which
|
||||
structures appear in each dimension. Crafting recipes are re-learned when they are received from other players as item
|
||||
checks, and occasionally when completing your own achievements.
|
||||
checks, and occasionally when completing your own achievements. See below for which recipes are shuffled.
|
||||
|
||||
## What is considered a location check in minecraft?
|
||||
|
||||
@@ -25,3 +25,86 @@ inventory directly.
|
||||
|
||||
Victory is achieved when the player kills the Ender Dragon, enters the portal in The End, and completes the credits
|
||||
sequence either by skipping it or watching hit play out.
|
||||
|
||||
## Which recipes are locked?
|
||||
|
||||
* Archery
|
||||
* Bow
|
||||
* Arrow
|
||||
* Crossbow
|
||||
* Brewing
|
||||
* Blaze Powder
|
||||
* Brewing Stand
|
||||
* Enchanting
|
||||
* Enchanting Table
|
||||
* Bookshelf
|
||||
* Bucket
|
||||
* Flint & Steel
|
||||
* All Beds
|
||||
* Bottles
|
||||
* Shield
|
||||
* Fishing Rod
|
||||
* Fishing Rod
|
||||
* Carrot on a Stick
|
||||
* Warped Fungus on a Stick
|
||||
* Campfire
|
||||
* Campfire
|
||||
* Soul Campfire
|
||||
* Spyglass
|
||||
* Lead
|
||||
* Progressive Weapons
|
||||
* Tier I
|
||||
* Stone Sword
|
||||
* Stone Axe
|
||||
* Tier II
|
||||
* Iron Sword
|
||||
* Iron Axe
|
||||
* Tier III
|
||||
* Diamond Sword
|
||||
* Diamond Axe
|
||||
* Progessive Tools
|
||||
* Tier I
|
||||
* Stone Shovel
|
||||
* Stone Hoe
|
||||
* Tier II
|
||||
* Iron Shovel
|
||||
* Iron Hoe
|
||||
* Tier III
|
||||
* Diamond Shovel
|
||||
* Diamond Hoe
|
||||
* Netherite Ingot
|
||||
* Progressive Armor
|
||||
* Tier I
|
||||
* Iron Helmet
|
||||
* Iron Chestplate
|
||||
* Iron Leggings
|
||||
* Iron Boots
|
||||
* Tier II
|
||||
* Diamond Helmet
|
||||
* Diamond Chestplate
|
||||
* Diamond Leggings
|
||||
* Diamond Boots
|
||||
* Progressive Resource Crafting
|
||||
* Tier I
|
||||
* Iron Ingot from Nuggets
|
||||
* Iron Nugget
|
||||
* Gold Ingot from Nuggets
|
||||
* Gold Nugget
|
||||
* Furnace
|
||||
* Blast Furnace
|
||||
* Tier II
|
||||
* Redstone
|
||||
* Redstone Block
|
||||
* Glowstone
|
||||
* Iron Ingot from Iron Block
|
||||
* Iron Block
|
||||
* Gold Ingot from Gold Block
|
||||
* Gold Block
|
||||
* Diamond
|
||||
* Diamond Block
|
||||
* Netherite Block
|
||||
* Netherite Ingot from Netherite Block
|
||||
* Anvil
|
||||
* Emerald
|
||||
* Emerald Block
|
||||
* Copper Block
|
||||
|
||||
@@ -1388,6 +1388,10 @@ def get_pool_core(world):
|
||||
remove_junk_pool = list(remove_junk_pool) + ['Recovery Heart', 'Bombs (20)', 'Arrows (30)', 'Ice Trap']
|
||||
|
||||
junk_candidates = [item for item in pool if item in remove_junk_pool]
|
||||
if len(pending_junk_pool) > len(junk_candidates):
|
||||
excess = len(pending_junk_pool) - len(junk_candidates)
|
||||
if world.triforce_hunt:
|
||||
raise RuntimeError(f"Items in the pool for player {world.player} exceed locations. Add {excess} location(s) or remove {excess} triforce piece(s).")
|
||||
while pending_junk_pool:
|
||||
pending_item = pending_junk_pool.pop()
|
||||
if not junk_candidates:
|
||||
|
||||
@@ -49,7 +49,6 @@ class OOTItem(Item):
|
||||
self.type = type
|
||||
self.index = index
|
||||
self.special = special or {}
|
||||
self.looks_like_item = None
|
||||
self.price = special.get('price', None) if special else None
|
||||
self.internal = False
|
||||
|
||||
|
||||
@@ -101,7 +101,6 @@ class InteriorEntrances(Choice):
|
||||
option_off = 0
|
||||
option_simple = 1
|
||||
option_all = 2
|
||||
alias_false = 0
|
||||
alias_true = 2
|
||||
|
||||
|
||||
@@ -141,7 +140,6 @@ class MixEntrancePools(Choice):
|
||||
option_off = 0
|
||||
option_indoor = 1
|
||||
option_all = 2
|
||||
alias_false = 0
|
||||
|
||||
|
||||
class DecoupleEntrances(Toggle):
|
||||
@@ -158,12 +156,12 @@ class TriforceGoal(Range):
|
||||
"""Number of Triforce pieces required to complete the game."""
|
||||
display_name = "Required Triforce Pieces"
|
||||
range_start = 1
|
||||
range_end = 100
|
||||
range_end = 80
|
||||
default = 20
|
||||
|
||||
|
||||
class ExtraTriforces(Range):
|
||||
"""Percentage of additional Triforce pieces in the pool, separate from the item pool setting."""
|
||||
"""Percentage of additional Triforce pieces in the pool. With high numbers, you may need to randomize additional locations to have enough items."""
|
||||
display_name = "Percentage of Extra Triforce Pieces"
|
||||
range_start = 0
|
||||
range_end = 100
|
||||
@@ -308,7 +306,6 @@ class ShopShuffle(Choice):
|
||||
option_off = 0
|
||||
option_fixed_number = 1
|
||||
option_random_number = 2
|
||||
alias_false = 0
|
||||
|
||||
|
||||
class ShopSlots(Range):
|
||||
@@ -326,7 +323,6 @@ class TokenShuffle(Choice):
|
||||
option_dungeons = 1
|
||||
option_overworld = 2
|
||||
option_all = 3
|
||||
alias_false = 0
|
||||
|
||||
|
||||
class ScrubShuffle(Choice):
|
||||
@@ -336,7 +332,6 @@ class ScrubShuffle(Choice):
|
||||
option_low = 1
|
||||
option_regular = 2
|
||||
option_random_prices = 3
|
||||
alias_false = 0
|
||||
alias_affordable = 1
|
||||
alias_expensive = 2
|
||||
|
||||
@@ -569,7 +564,6 @@ class Hints(Choice):
|
||||
option_agony = 2
|
||||
option_always = 3
|
||||
default = 3
|
||||
alias_false = 0
|
||||
|
||||
|
||||
class MiscHints(DefaultOnToggle):
|
||||
@@ -673,8 +667,6 @@ class IceTraps(Choice):
|
||||
option_mayhem = 3
|
||||
option_onslaught = 4
|
||||
default = 1
|
||||
alias_false = 0
|
||||
alias_true = 2
|
||||
alias_extra = 2
|
||||
|
||||
|
||||
@@ -742,7 +734,6 @@ class Music(Choice):
|
||||
option_normal = 0
|
||||
option_off = 1
|
||||
option_randomized = 2
|
||||
alias_false = 1
|
||||
|
||||
|
||||
class BackgroundMusic(Music):
|
||||
|
||||
@@ -1844,7 +1844,7 @@ def write_rom_item(rom, item_id, item):
|
||||
|
||||
|
||||
def get_override_table(world):
|
||||
return list(filter(lambda val: val != None, map(partial(get_override_entry, world.player), world.world.get_filled_locations(world.player))))
|
||||
return list(filter(lambda val: val != None, map(partial(get_override_entry, world), world.world.get_filled_locations(world.player))))
|
||||
|
||||
|
||||
override_struct = struct.Struct('>xBBBHBB') # match override_t in get_items.c
|
||||
@@ -1852,10 +1852,10 @@ def get_override_table_bytes(override_table):
|
||||
return b''.join(sorted(itertools.starmap(override_struct.pack, override_table)))
|
||||
|
||||
|
||||
def get_override_entry(player_id, location):
|
||||
def get_override_entry(ootworld, location):
|
||||
scene = location.scene
|
||||
default = location.default
|
||||
player_id = 0 if player_id == location.item.player else min(location.item.player, 255)
|
||||
player_id = 0 if ootworld.player == location.item.player else min(location.item.player, 255)
|
||||
if location.item.game != 'Ocarina of Time':
|
||||
# This is an AP sendable. It's guaranteed to not be None.
|
||||
if location.item.advancement:
|
||||
@@ -1869,7 +1869,7 @@ def get_override_entry(player_id, location):
|
||||
|
||||
if location.item.trap:
|
||||
item_id = 0x7C # Ice Trap ID, to get "X is a fool" message
|
||||
looks_like_item_id = location.item.looks_like_item.index
|
||||
looks_like_item_id = ootworld.trap_appearances[location.address].index
|
||||
else:
|
||||
looks_like_item_id = 0
|
||||
|
||||
@@ -2091,7 +2091,8 @@ def get_locked_doors(rom, world):
|
||||
return [0x00D4 + scene * 0x1C + 0x04 + flag_byte, flag_bits]
|
||||
|
||||
# If boss door, set the door's unlock flag
|
||||
if (world.shuffle_bosskeys == 'remove' and scene != 0x0A) or (world.shuffle_ganon_bosskey == 'remove' and scene == 0x0A):
|
||||
if (world.shuffle_bosskeys == 'remove' and scene != 0x0A) or (
|
||||
world.shuffle_ganon_bosskey == 'remove' and scene == 0x0A and not world.triforce_hunt):
|
||||
if actor_id == 0x002E and actor_type == 0x05:
|
||||
return [0x00D4 + scene * 0x1C + 0x04 + flag_byte, flag_bits]
|
||||
|
||||
@@ -2109,23 +2110,20 @@ def place_shop_items(rom, world, shop_items, messages, locations, init_shop_id=F
|
||||
rom.write_int16(location.address1, location.item.index)
|
||||
else:
|
||||
if location.item.trap:
|
||||
item_display = location.item.looks_like_item
|
||||
elif location.item.game != "Ocarina of Time":
|
||||
item_display = location.item
|
||||
if location.item.advancement:
|
||||
item_display.index = 0xCB
|
||||
else:
|
||||
item_display.index = 0xCC
|
||||
item_display.special = {}
|
||||
item_display = world.trap_appearances[location.address]
|
||||
else:
|
||||
item_display = location.item
|
||||
|
||||
# bottles in shops should look like empty bottles
|
||||
# so that that are different than normal shop refils
|
||||
if 'shop_object' in item_display.special:
|
||||
rom_item = read_rom_item(rom, item_display.special['shop_object'])
|
||||
if location.item.trap or location.item.game == "Ocarina of Time":
|
||||
if 'shop_object' in item_display.special:
|
||||
rom_item = read_rom_item(rom, item_display.special['shop_object'])
|
||||
else:
|
||||
rom_item = read_rom_item(rom, item_display.index)
|
||||
else:
|
||||
rom_item = read_rom_item(rom, item_display.index)
|
||||
display_index = 0xCB if location.item.advancement else 0xCC
|
||||
rom_item = read_rom_item(rom, display_index)
|
||||
|
||||
shop_objs.add(rom_item['object_id'])
|
||||
shop_id = world.current_shop_id
|
||||
|
||||
@@ -178,6 +178,10 @@ class OOTWorld(World):
|
||||
if self.skip_child_zelda:
|
||||
self.shuffle_weird_egg = False
|
||||
|
||||
# Ganon boss key should not be in itempool in triforce hunt
|
||||
if self.triforce_hunt:
|
||||
self.shuffle_ganon_bosskey = 'remove'
|
||||
|
||||
# Determine skipped trials in GT
|
||||
# This needs to be done before the logic rules in GT are parsed
|
||||
trial_list = ['Forest', 'Fire', 'Water', 'Spirit', 'Shadow', 'Light']
|
||||
@@ -186,7 +190,11 @@ class OOTWorld(World):
|
||||
|
||||
# Determine which dungeons are MQ
|
||||
# Possible future plan: allow user to pick which dungeons are MQ
|
||||
mq_dungeons = self.world.random.sample(dungeon_table, self.mq_dungeons)
|
||||
if self.logic_rules == 'glitchless':
|
||||
mq_dungeons = self.world.random.sample(dungeon_table, self.mq_dungeons)
|
||||
else:
|
||||
self.mq_dungeons = 0
|
||||
mq_dungeons = []
|
||||
self.dungeon_mq = {item['name']: (item in mq_dungeons) for item in dungeon_table}
|
||||
|
||||
# Determine tricks in logic
|
||||
@@ -803,9 +811,10 @@ class OOTWorld(World):
|
||||
|
||||
with i_o_limiter:
|
||||
# Make traps appear as other random items
|
||||
ice_traps = [loc.item for loc in self.get_locations() if loc.item.trap]
|
||||
for trap in ice_traps:
|
||||
trap.looks_like_item = self.create_item(self.world.slot_seeds[self.player].choice(self.fake_items).name)
|
||||
trap_location_ids = [loc.address for loc in self.get_locations() if loc.item.trap]
|
||||
self.trap_appearances = {}
|
||||
for loc_id in trap_location_ids:
|
||||
self.trap_appearances[loc_id] = self.create_item(self.world.slot_seeds[self.player].choice(self.fake_items).name)
|
||||
|
||||
# Seed hint RNG, used for ganon text lines also
|
||||
self.hint_rng = self.world.slot_seeds[self.player]
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
from Options import Range, Toggle, DefaultOnToggle, Choice
|
||||
|
||||
class UseResourcePacks(DefaultOnToggle):
|
||||
"""Uses Resource Packs to fill out the item pool from Raft. Resource Packs have basic earlygame items such as planks, plastic, or food."""
|
||||
display_name = "Use resource packs"
|
||||
from Options import Range, Toggle, DefaultOnToggle, Choice, DeathLink
|
||||
|
||||
class MinimumResourcePackAmount(Range):
|
||||
"""The minimum amount of resources available in a resource pack"""
|
||||
@@ -19,23 +15,30 @@ class MaximumResourcePackAmount(Range):
|
||||
default = 5
|
||||
|
||||
class DuplicateItems(Choice):
|
||||
"""Adds duplicates of items to the item pool. These will be selected alongside
|
||||
Resource Packs (if configured). Note that there are not many progression items,
|
||||
and selecting Progression may produce many of the same duplicate item."""
|
||||
"""Adds duplicates of items to the item pool (if configured in Filler items). These will be selected alongside Resource Packs (if configured). Note that there are not many progression items, and selecting Progression may produce many of the same duplicate item."""
|
||||
display_name = "Duplicate items"
|
||||
option_disabled = 0
|
||||
option_progression = 1
|
||||
option_non_progression = 2
|
||||
option_any = 3
|
||||
option_progression = 0
|
||||
option_non_progression = 1
|
||||
option_any = 2
|
||||
default = 2
|
||||
|
||||
class FillerItemTypes(Choice):
|
||||
"""Determines whether to use Resource Packs, Duplicate Items (as configured), or both."""
|
||||
display_name = "Filler items"
|
||||
option_resource_packs = 0
|
||||
option_duplicates = 1
|
||||
option_both = 2
|
||||
|
||||
class IslandFrequencyLocations(Choice):
|
||||
"""Sets where frequencies for story islands are located."""
|
||||
display_name = "Frequency locations"
|
||||
option_vanilla = 0
|
||||
option_random_on_island = 1
|
||||
option_progressive = 2
|
||||
option_anywhere = 3
|
||||
default = 1
|
||||
option_random_island_order = 2
|
||||
option_random_on_island_random_order = 3
|
||||
option_progressive = 4
|
||||
option_anywhere = 5
|
||||
default = 2
|
||||
|
||||
class IslandGenerationDistance(Choice):
|
||||
"""Sets how far away islands spawn from you when you input their coordinates into the Receiver."""
|
||||
@@ -56,7 +59,7 @@ class ProgressiveItems(DefaultOnToggle):
|
||||
display_name = "Progressive items"
|
||||
|
||||
class BigIslandEarlyCrafting(Toggle):
|
||||
"""Allows recipes that require items from big islands (eg leather) to lock earlygame items like the Receiver, Bolt, or Smelter."""
|
||||
"""Allows recipes that require items from big islands (eg leather) to lock earlygame items like the Receiver, Bolt, or Smelter. Big islands are available from the start of the game, however it can take a long time to find them."""
|
||||
display_name = "Early recipes behind big islands"
|
||||
|
||||
class PaddleboardMode(Toggle):
|
||||
@@ -64,14 +67,15 @@ class PaddleboardMode(Toggle):
|
||||
display_name = "Paddleboard Mode"
|
||||
|
||||
raft_options = {
|
||||
"use_resource_packs": UseResourcePacks,
|
||||
"minimum_resource_pack_amount": MinimumResourcePackAmount,
|
||||
"maximum_resource_pack_amount": MaximumResourcePackAmount,
|
||||
"duplicate_items": DuplicateItems,
|
||||
"filler_item_types": FillerItemTypes,
|
||||
"island_frequency_locations": IslandFrequencyLocations,
|
||||
"island_generation_distance": IslandGenerationDistance,
|
||||
"expensive_research": ExpensiveResearch,
|
||||
"progressive_items": ProgressiveItems,
|
||||
"big_island_early_crafting": BigIslandEarlyCrafting,
|
||||
"paddleboard_mode": PaddleboardMode
|
||||
"paddleboard_mode": PaddleboardMode,
|
||||
"death_link": DeathLink
|
||||
}
|
||||
|
||||
@@ -12,9 +12,6 @@ class RaftLogic(LogicMixin):
|
||||
|
||||
def raft_can_smelt_items(self, player):
|
||||
return self.has("Smelter", player)
|
||||
|
||||
def raft_can_find_titanium(self, player):
|
||||
return self.has("Metal detector", player)
|
||||
|
||||
def raft_can_craft_bolt(self, player):
|
||||
return self.raft_can_smelt_items(player) and self.has("Bolt", player)
|
||||
@@ -27,12 +24,19 @@ class RaftLogic(LogicMixin):
|
||||
|
||||
def raft_can_craft_circuitBoard(self, player):
|
||||
return self.raft_can_smelt_items(player) and self.has("Circuit board", player)
|
||||
|
||||
def raft_can_craft_shovel(self, player):
|
||||
return self.raft_can_smelt_items(player) and self.has("Shovel", player) and self.raft_can_craft_bolt(player)
|
||||
|
||||
def raft_can_craft_reciever(self, player):
|
||||
return self.raft_can_craft_circuitBoard(player) and self.raft_can_craft_hinge(player) and self.has("Receiver", player)
|
||||
|
||||
def raft_can_craft_antenna(self, player):
|
||||
return self.raft_can_craft_circuitBoard(player) and self.raft_can_craft_bolt(player) and self.has("Antenna", player)
|
||||
|
||||
def raft_can_find_titanium(self, player):
|
||||
return (self.has("Metal detector", player) and self.raft_can_craft_battery(player)
|
||||
and self.raft_can_craft_shovel(player))
|
||||
|
||||
def raft_can_craft_plasticBottle(self, player):
|
||||
return self.raft_can_smelt_items(player) and self.has("Empty bottle", player)
|
||||
@@ -60,7 +64,7 @@ class RaftLogic(LogicMixin):
|
||||
return self.raft_can_craft_hinge(player) and self.raft_can_craft_bolt(player) and self.has("Zipline tool", player)
|
||||
|
||||
def raft_can_get_dirt(self, player):
|
||||
return self.raft_can_smelt_items(player) and self.raft_can_craft_bolt(player) and self.has("Shovel", player)
|
||||
return self.raft_can_craft_shovel(player) and self.raft_big_islands_available(player)
|
||||
|
||||
def raft_can_craft_grassPlot(self, player):
|
||||
return self.raft_can_get_dirt(player) and self.has("Grass plot", player)
|
||||
@@ -88,60 +92,69 @@ class RaftLogic(LogicMixin):
|
||||
return self.raft_can_access_radio_tower(player)
|
||||
|
||||
def raft_can_access_vasagatan(self, player):
|
||||
return self.raft_can_complete_radio_tower(player) and self.raft_can_navigate(player) and self.has("Vasagatan Frequency", player)
|
||||
return self.raft_can_navigate(player) and self.has("Vasagatan Frequency", player)
|
||||
|
||||
def raft_can_complete_vasagatan(self, player):
|
||||
return self.raft_can_access_vasagatan(player)
|
||||
|
||||
def raft_can_access_balboa_island(self, player):
|
||||
return (self.raft_can_complete_vasagatan(player)
|
||||
and self.raft_can_drive(player)
|
||||
and self.has("Balboa Island Frequency", player))
|
||||
return self.raft_can_drive(player) and self.has("Balboa Island Frequency", player)
|
||||
|
||||
def raft_can_complete_balboa_island(self, player):
|
||||
return self.raft_can_access_balboa_island(player) and self.raft_can_craft_machete(player)
|
||||
|
||||
def raft_can_access_caravan_island(self, player):
|
||||
return self.raft_can_complete_balboa_island(player) and self.raft_can_drive(player) and self.has("Caravan Island Frequency", player)
|
||||
return self.raft_can_drive(player) and self.has("Caravan Island Frequency", player)
|
||||
|
||||
def raft_can_complete_caravan_island(self, player):
|
||||
return self.raft_can_access_caravan_island(player) and self.raft_can_craft_ziplineTool(player)
|
||||
|
||||
def raft_can_access_tangaroa(self, player):
|
||||
return self.raft_can_complete_caravan_island(player) and self.raft_can_drive(player) and self.has("Tangaroa Frequency", player)
|
||||
return self.raft_can_drive(player) and self.has("Tangaroa Frequency", player)
|
||||
|
||||
def raft_can_complete_tangaroa(self, player):
|
||||
return self.raft_can_access_tangaroa(player)
|
||||
return self.raft_can_access_tangaroa(player) and self.raft_can_craft_ziplineTool(player)
|
||||
|
||||
def raft_can_access_varuna_point(self, player):
|
||||
return self.raft_can_complete_tangaroa(player) and self.raft_can_drive(player) and self.has("Varuna Point Frequency", player)
|
||||
return self.raft_can_drive(player) and self.has("Varuna Point Frequency", player)
|
||||
|
||||
def raft_can_complete_varuna_point(self, player):
|
||||
return self.raft_can_access_varuna_point(player)
|
||||
return self.raft_can_access_varuna_point(player) and self.raft_can_craft_ziplineTool(player)
|
||||
|
||||
def raft_can_access_temperance(self, player):
|
||||
return self.raft_can_complete_varuna_point(player) and self.raft_can_drive(player) and self.has("Temperance Frequency", player)
|
||||
return self.raft_can_drive(player) and self.has("Temperance Frequency", player)
|
||||
|
||||
def raft_can_complete_temperance(self, player):
|
||||
return self.raft_can_access_temperance(player)
|
||||
return self.raft_can_access_temperance(player) # No zipline required on Temperance
|
||||
|
||||
def raft_can_access_utopia(self, player):
|
||||
return self.raft_can_complete_temperance(player) and self.raft_can_drive(player) and self.has("Utopia Frequency", player)
|
||||
return (self.raft_can_drive(player)
|
||||
# Access checks are to prevent frequencies for other
|
||||
# islands from appearing in Utopia
|
||||
and self.raft_can_access_radio_tower(player)
|
||||
and self.raft_can_access_vasagatan(player)
|
||||
and self.raft_can_access_balboa_island(player)
|
||||
and self.raft_can_access_caravan_island(player)
|
||||
and self.raft_can_access_tangaroa(player)
|
||||
and self.raft_can_access_varuna_point(player)
|
||||
and self.raft_can_access_temperance(player)
|
||||
and self.has("Utopia Frequency", player)
|
||||
and self.raft_can_craft_shovel(player)) # Shovels are available but we don't want to softlock players
|
||||
|
||||
def raft_can_complete_utopia(self, player):
|
||||
return self.raft_can_access_utopia(player)
|
||||
return self.raft_can_access_utopia(player) and self.raft_can_craft_ziplineTool(player)
|
||||
|
||||
def set_rules(world, player):
|
||||
regionChecks = {
|
||||
"Raft": lambda state: True,
|
||||
"ResearchTable": lambda state: True,
|
||||
"RadioTower": lambda state: state.raft_can_access_radio_tower(player), # All can_access functions have state as implicit parameter for function
|
||||
"Vasagatan": lambda state: state.raft_can_complete_radio_tower(player) and state.raft_can_access_vasagatan(player),
|
||||
"BalboaIsland": lambda state: state.raft_can_complete_vasagatan(player) and state.raft_can_access_balboa_island(player),
|
||||
"CaravanIsland": lambda state: state.raft_can_complete_balboa_island(player) and state.raft_can_access_caravan_island(player),
|
||||
"Tangaroa": lambda state: state.raft_can_complete_caravan_island(player) and state.raft_can_access_tangaroa(player),
|
||||
"Varuna Point": lambda state: state.raft_can_complete_tangaroa(player) and state.raft_can_access_varuna_point(player),
|
||||
"Temperance": lambda state: state.raft_can_complete_varuna_point(player) and state.raft_can_access_temperance(player),
|
||||
"Vasagatan": lambda state: state.raft_can_access_vasagatan(player),
|
||||
"BalboaIsland": lambda state: state.raft_can_access_balboa_island(player),
|
||||
"CaravanIsland": lambda state: state.raft_can_access_caravan_island(player),
|
||||
"Tangaroa": lambda state: state.raft_can_access_tangaroa(player),
|
||||
"Varuna Point": lambda state: state.raft_can_access_varuna_point(player),
|
||||
"Temperance": lambda state: state.raft_can_access_temperance(player),
|
||||
"Utopia": lambda state: state.raft_can_complete_temperance(player) and state.raft_can_access_utopia(player)
|
||||
}
|
||||
itemChecks = {
|
||||
@@ -183,7 +196,7 @@ def set_rules(world, player):
|
||||
if region != "Menu":
|
||||
for exitRegion in world.get_region(region, player).exits:
|
||||
set_rule(world.get_entrance(exitRegion.name, player), regionChecks[region])
|
||||
|
||||
|
||||
# Location access rules
|
||||
for location in location_table:
|
||||
locFromWorld = world.get_location(location["name"], player)
|
||||
|
||||
@@ -56,21 +56,21 @@ class RaftWorld(World):
|
||||
extraItemNamePool = []
|
||||
extras = len(location_table) - len(item_table) - 1 # Victory takes up 1 unaccounted-for slot
|
||||
if extras > 0:
|
||||
if (self.world.use_resource_packs[self.player].value):
|
||||
if (self.world.filler_item_types[self.player].value != 1): # Use resource packs
|
||||
for packItem in resourcePackItems:
|
||||
for i in range(minimumResourcePackAmount, maximumResourcePackAmount + 1):
|
||||
extraItemNamePool.append(createResourcePackName(i, packItem))
|
||||
|
||||
if self.world.duplicate_items[self.player].value != 0:
|
||||
if self.world.filler_item_types[self.player].value != 0: # Use duplicate items
|
||||
dupeItemPool = item_table.copy()
|
||||
# Remove frequencies if necessary
|
||||
if self.world.island_frequency_locations[self.player].value != 3: # Not completely random locations
|
||||
if self.world.island_frequency_locations[self.player].value != 5: # Not completely random locations
|
||||
dupeItemPool = (itm for itm in dupeItemPool if "Frequency" not in itm["name"])
|
||||
|
||||
# Remove progression or non-progression items if necessary
|
||||
if (self.world.duplicate_items[self.player].value == 1): # Progression only
|
||||
if (self.world.duplicate_items[self.player].value == 0): # Progression only
|
||||
dupeItemPool = (itm for itm in dupeItemPool if itm["progression"] == True)
|
||||
elif (self.world.duplicate_items[self.player].value == 2): # Non-progression only
|
||||
elif (self.world.duplicate_items[self.player].value == 1): # Non-progression only
|
||||
dupeItemPool = (itm for itm in dupeItemPool if itm["progression"] == False)
|
||||
|
||||
dupeItemPool = list(dupeItemPool)
|
||||
@@ -91,19 +91,15 @@ class RaftWorld(World):
|
||||
|
||||
def create_regions(self):
|
||||
create_regions(self.world, self.player)
|
||||
|
||||
def fill_slot_data(self):
|
||||
slot_data = {}
|
||||
return slot_data
|
||||
|
||||
def get_pre_fill_items(self):
|
||||
if self.world.island_frequency_locations[self.player] in [0, 1]:
|
||||
if self.world.island_frequency_locations[self.player] in [0, 1, 2, 3]:
|
||||
return [loc.item for loc in self.world.get_filled_locations()]
|
||||
return []
|
||||
|
||||
def create_item_replaceAsNecessary(self, name: str) -> Item:
|
||||
isFrequency = "Frequency" in name
|
||||
shouldUseProgressive = ((isFrequency and self.world.island_frequency_locations[self.player].value == 2)
|
||||
shouldUseProgressive = ((isFrequency and self.world.island_frequency_locations[self.player].value == 4)
|
||||
or (not isFrequency and self.world.progressive_items[self.player].value))
|
||||
if shouldUseProgressive and name in progressive_table:
|
||||
name = progressive_table[name]
|
||||
@@ -148,6 +144,40 @@ class RaftWorld(World):
|
||||
self.setLocationItemFromRegion("Tangaroa", "Varuna Point Frequency")
|
||||
self.setLocationItemFromRegion("Varuna Point", "Temperance Frequency")
|
||||
self.setLocationItemFromRegion("Temperance", "Utopia Frequency")
|
||||
elif self.world.island_frequency_locations[self.player] in [2, 3]:
|
||||
locationToFrequencyItemMap = {
|
||||
"Vasagatan": "Vasagatan Frequency",
|
||||
"BalboaIsland": "Balboa Island Frequency",
|
||||
"CaravanIsland": "Caravan Island Frequency",
|
||||
"Tangaroa": "Tangaroa Frequency",
|
||||
"Varuna Point": "Varuna Point Frequency",
|
||||
"Temperance": "Temperance Frequency",
|
||||
"Utopia": "Utopia Frequency"
|
||||
}
|
||||
locationToVanillaFrequencyLocationMap = {
|
||||
"RadioTower": "Radio Tower Frequency to Vasagatan",
|
||||
"Vasagatan": "Vasagatan Frequency to Balboa",
|
||||
"BalboaIsland": "Relay Station quest",
|
||||
"CaravanIsland": "Caravan Island Frequency to Tangaroa",
|
||||
"Tangaroa": "Tangaroa Frequency to Varuna Point",
|
||||
"Varuna Point": "Varuna Point Frequency to Temperance",
|
||||
"Temperance": "Temperance Frequency to Utopia"
|
||||
}
|
||||
# Utopia is never chosen until the end, otherwise these are chosen randomly
|
||||
availableLocationList = ["Vasagatan", "BalboaIsland", "CaravanIsland", "Tangaroa", "Varuna Point", "Temperance", "Utopia"]
|
||||
previousLocation = "RadioTower"
|
||||
while (len(availableLocationList) > 0):
|
||||
if (len(availableLocationList) > 1):
|
||||
currentLocation = availableLocationList[random.randint(0, len(availableLocationList) - 2)]
|
||||
else:
|
||||
currentLocation = availableLocationList[0] # Utopia (only one left in list)
|
||||
availableLocationList.remove(currentLocation)
|
||||
if self.world.island_frequency_locations[self.player] == 2:
|
||||
self.setLocationItem(locationToVanillaFrequencyLocationMap[previousLocation], locationToFrequencyItemMap[currentLocation])
|
||||
elif self.world.island_frequency_locations[self.player] == 3:
|
||||
self.setLocationItemFromRegion(previousLocation, locationToFrequencyItemMap[currentLocation])
|
||||
previousLocation = currentLocation
|
||||
|
||||
# Victory item
|
||||
self.world.get_location("Utopia Complete", self.player).place_locked_item(
|
||||
RaftItem("Victory", ItemClassification.progression, None, player=self.player))
|
||||
@@ -166,7 +196,8 @@ class RaftWorld(World):
|
||||
def fill_slot_data(self):
|
||||
return {
|
||||
"IslandGenerationDistance": self.world.island_generation_distance[self.player].value,
|
||||
"ExpensiveResearch": self.world.expensive_research[self.player].value
|
||||
"ExpensiveResearch": bool(self.world.expensive_research[self.player].value),
|
||||
"DeathLink": bool(self.world.death_link[self.player].value)
|
||||
}
|
||||
|
||||
def create_region(world: MultiWorld, player: int, name: str, locations=None, exits=None):
|
||||
|
||||
@@ -22,7 +22,7 @@ Decoration Packages are unchanged.
|
||||
Researches and pickups remain visually unchanged, regardless of what the unlock is.
|
||||
|
||||
## When the player receives an item, what happens?
|
||||
A Raft notification will appear with the item information. The unlock will also appear in the chat. Unlocks that would normally give you the item (eg Machete) will NOT give it to you, but must instead be crafted.
|
||||
A Raft notification will appear with the item information. The unlock will also appear in the chat. Unlocks that would normally give you the item (eg Zipline) will NOT give it to you, but must instead be crafted.
|
||||
|
||||
## Are there any limitations compared to vanilla Raft?
|
||||
- Mods that add new researchable technologies, modify story islands, or give items like blueprints are likely incompatible with Raftipelago.
|
||||
|
||||
@@ -4,23 +4,50 @@
|
||||
|
||||
- [Raft](https://store.steampowered.com/app/648800/Raft/)
|
||||
- [Raft Mod Loader](https://www.raftmodding.com/loader) ("*RML*")
|
||||
- [ModUtils mod](https://www.raftmodding.com/mods/modutils)
|
||||
- [Raftipelago mod](https://www.raftmodding.com/mods/raftipelago)
|
||||
|
||||
## Installation Procedures
|
||||
|
||||
1. Install Raft. The currently-supported Raft version is Version 1.0: The Final Chapter. If you plan on playing Raft mainly with Archipelago, it's recommended to disable Raft auto-updating through Steam, as there is no beta channel to get old builds.
|
||||
1. Install Raft. The currently-supported Raft version is Version 1.0: The Final Chapter. Any minor version (such as 1.08) should be compatible.
|
||||
|
||||
2. Install RML.
|
||||
|
||||
3. Install the Raftipelago mod from the Raft Modding website. You should open the auto-installation link on the webpage through RML. Alternatively, you can download the .rmod file and place it in the Mods folder manually.
|
||||
3. Install the Raftipelago and ModUtils mods from the Raft Modding website. You should open the auto-installation link on the webpage through RML. Alternatively, you can download the .rmod file and place it in the Mods folder manually.
|
||||
|
||||
4. Open RML and click Play. If you've already installed it, the shortcut in the Start Menu is called "RMLLauncher.exe". Raft should start.
|
||||
4. Open RML and click Play. If you've already installed it, the executable that was used to install RML ("RMLLauncher.exe" unless renamed) should be used to run RML. Raft should start after clicking Play.
|
||||
|
||||
5. Open the RML menu. This should open automatically when Raft first loads. If it does not, and you see RML information in the top center of the Raft main menu, press F9 to open it.
|
||||
|
||||
6. Navigate to the "Mod manager" tab in the left-hand menu.
|
||||
|
||||
7. Click on the plug icon for Raftipelago to load the mod.
|
||||
7. Click on the plug icon for ModUtils to load the mod. You can also click on the (i) next to the plug icon, then check the "Load this mod at startup" button. This will make the mod always load at startup.
|
||||
|
||||
8. Click on the plug icon for Raftipelago to load the mod. While it's possible to also make this mod load at startup, it's recommended *not* to do so; if this mod loads before ModUtils, the mod will fail to load properly.
|
||||
|
||||
## Joining a MultiWorld Game
|
||||
|
||||
1. Ensure you're on the Main Menu with Raftipelago loaded.
|
||||
|
||||
2. Open the Debug Console by pressing F10.
|
||||
|
||||
3. Type */connect {serverAddress} {username} {password}* into the console and hit Enter.
|
||||
- Example: */connect archipelago.gg:12345 SunnyBat*
|
||||
- If there is no password, the password argument may be omitted (as is the case in the above example).
|
||||
- serverAddress must not contain spaces.
|
||||
- If your username or password contains spaces, surround that value with quotation marks ("). Adding quotation marks even when not necessary (eg "SunnyBat") is fine.
|
||||
- If your username or password starts with a quotation mark, surround the value with an additional set of quotation marks (eg the value *"myP@s$w0rD* would be entered as *""myP@s$w0rD"*).
|
||||
|
||||
4. Start a new game or load an existing one. It's recommended to avoid using an existing game that was not created with your current run of Raftipelago (either vanilla or a different Raftipelago run). It will work, but if anything is unlocked, it will be automatically registered with Archipelago once the world is loaded. This is irreversible.
|
||||
|
||||
5. You can disconnect from an Archipelago server by typing */disconnect confirmDisconnect* into the console and hitting Enter.
|
||||
|
||||
## Multiplayer Raft
|
||||
|
||||
You're able to have multiple Raft players on a single Raftipelago world. This will work, with a few notes:
|
||||
- Only the player that creates/loads the world can connect to Archipelago (this is the "host" of the Raft world). Other players do not need to connect; everything will be routed through the the host.
|
||||
- Players other than the host will be labeled as a "Raft Player (Steam name)" when using ingame chat, which will be routed through Archipelago chat.
|
||||
- Ingame chat will only work when the host is connected to the Archipelago server.
|
||||
|
||||
## Installation Troubleshooting
|
||||
|
||||
@@ -45,38 +72,12 @@ If this happens, then RML is configured to only start a new instance of Raft, th
|
||||
You can either:
|
||||
- Close the existing instance of Raft then click Play
|
||||
- Check the box next to the "Disable Automatic Game Start" setting in the Settings menu then click Play.
|
||||
|
||||
## Joining a MultiWorld Game
|
||||
|
||||
1. Ensure you're on the Main Menu with Raftipelago loaded.
|
||||
|
||||
2. Open the Debug Console by pressing F10.
|
||||
|
||||
3. Type */connect {serverAddress} {username} {password}* into the console and hit Enter.
|
||||
- Example: */connect archipelago.gg:12345 SunnyBat*
|
||||
- serverAddress must not contain spaces.
|
||||
- If your username or password contains spaces, surround that value with quotation marks ("). Adding quotation marks even when not necessary (eg "SunnyBat") is fine.
|
||||
- If your username or password starts with a quotation mark, surround the value with an additional set of quotation marks (eg the value *"myP@s$w0rD* would be entered as *""myP@s$w0rD"*).
|
||||
|
||||
4. Start a new game or load an existing one.
|
||||
- Raftipelago save games are marked as *incompatible* with vanilla Raft. This means when Raftipelago is not loaded, saves made with Raftipelago will show as corrupt/unselectable.
|
||||
- Avoid using an existing game that was not created with your current run of Raftipelago (either vanilla or a different Raftipelago run). It will work, but if anything is unlocked, it will be automatically registered with Archipelago once the world is loaded. This is irreversible.
|
||||
|
||||
5. You can disconnect from an Archipelago server by typing */disconnect confirmDisconnect* into the console and hitting Enter.
|
||||
|
||||
## Multiplayer Raft
|
||||
|
||||
You're able to have multiple Raft players on a single Raftipelago world. This will work, with a few notes:
|
||||
- Only the player that creates/loads the world can connect to Archipelago (this is the "host" of the Raft world). Other players do not need to connect; everything will be routed through the the host.
|
||||
- Resource Packs are only received by the host and any other players connected to the Raft world when the resource pack is received.
|
||||
- Players other than the host will be labeled as a "Raft Player (Steam name)" when using ingame chat, which will be routed through Archipelago chat.
|
||||
- Ingame chat will only work when the host is connected to the Archipelago server.
|
||||
|
||||
## Game Troubleshooting
|
||||
|
||||
### The "Load game" button is disabled for my world / my world is corrupt
|
||||
|
||||
Be sure that you click the "Load game" button **after** you load Raftipelago. You can click the Load Game button again to reload all of the saves in your folder (there is no need to restart Raft if the mod loaded successfully).
|
||||
Be sure that you click the "Load game" button **after** you load Raftipelago. You can click the Load Game button again to refresh all of the saves in your folder (there is no need to restart Raft if the mod loaded successfully).
|
||||
|
||||
### I'm certain I'm doing things correctly, but the world is still not loadable
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"Raft": ["RadioTower", "ResearchTable"],
|
||||
"Raft": ["ResearchTable", "RadioTower", "Vasagatan", "BalboaIsland", "CaravanIsland", "Tangaroa", "Varuna Point", "Temperance", "Utopia"],
|
||||
"ResearchTable": [],
|
||||
"RadioTower": ["Vasagatan"],
|
||||
"Vasagatan": ["BalboaIsland"],
|
||||
"BalboaIsland": ["CaravanIsland"],
|
||||
"CaravanIsland": ["Tangaroa"],
|
||||
"Tangaroa": ["Varuna Point"],
|
||||
"Varuna Point": ["Temperance"],
|
||||
"Temperance": ["Utopia"],
|
||||
"RadioTower": [],
|
||||
"Vasagatan": [],
|
||||
"BalboaIsland": [],
|
||||
"CaravanIsland": [],
|
||||
"Tangaroa": [],
|
||||
"Varuna Point": [],
|
||||
"Temperance": [],
|
||||
"Utopia": []
|
||||
}
|
||||
@@ -46,7 +46,7 @@ def check_for_impossible_shuffle(shuffled_levels: typing.List[int], gate_0_range
|
||||
|
||||
class SA2BWorld(World):
|
||||
"""
|
||||
Sonic Adventure 2 Battle is an action platforming game. Play as Sonic, Tails, Knuckles, Shadow, Rogue, and Eggman across 31 stages and prevent the destruction of the earth.
|
||||
Sonic Adventure 2 Battle is an action platforming game. Play as Sonic, Tails, Knuckles, Shadow, Rouge, and Eggman across 31 stages and prevent the destruction of the earth.
|
||||
"""
|
||||
game: str = "Sonic Adventure 2 Battle"
|
||||
option_definitions = sa2b_options
|
||||
@@ -282,8 +282,7 @@ class SA2BWorld(World):
|
||||
spoiler_handle.writelines(text)
|
||||
|
||||
@classmethod
|
||||
def stage_fill_hook(cls, world, progitempool, nonexcludeditempool, localrestitempool, nonlocalrestitempool,
|
||||
restitempool, fill_locations):
|
||||
def stage_fill_hook(cls, world, progitempool, usefulitempool, filleritempool, fill_locations):
|
||||
if world.get_game_players("Sonic Adventure 2 Battle"):
|
||||
progitempool.sort(
|
||||
key=lambda item: 0 if (item.name != 'Emblem') else 1)
|
||||
|
||||
@@ -163,3 +163,17 @@ filler_items: typing.Tuple[str, ...] = (
|
||||
|
||||
lookup_id_to_name: typing.Dict[int, str] = {data.code: item_name for item_name, data in get_full_item_list().items() if
|
||||
data.code}
|
||||
# Map type to expected int
|
||||
type_flaggroups: typing.Dict[str, int] = {
|
||||
"Unit": 0,
|
||||
"Upgrade": 1,
|
||||
"Armory 1": 2,
|
||||
"Armory 2": 3,
|
||||
"Building": 4,
|
||||
"Mercenary": 5,
|
||||
"Laboratory": 6,
|
||||
"Protoss": 7,
|
||||
"Minerals": 8,
|
||||
"Vespene": 9,
|
||||
"Supply": 10,
|
||||
}
|
||||
|
||||
@@ -69,8 +69,8 @@ vanilla_mission_req_table = {
|
||||
"Zero Hour": MissionInfo(3, 4, [2], "Mar Sara", completion_critical=True),
|
||||
"Evacuation": MissionInfo(4, 4, [3], "Colonist"),
|
||||
"Outbreak": MissionInfo(5, 3, [4], "Colonist"),
|
||||
"Safe Haven": MissionInfo(6, 1, [5], "Colonist", number=7),
|
||||
"Haven's Fall": MissionInfo(7, 1, [5], "Colonist", number=7),
|
||||
"Safe Haven": MissionInfo(6, 4, [5], "Colonist", number=7),
|
||||
"Haven's Fall": MissionInfo(7, 4, [5], "Colonist", number=7),
|
||||
"Smash and Grab": MissionInfo(8, 5, [3], "Artifact", completion_critical=True),
|
||||
"The Dig": MissionInfo(9, 4, [8], "Artifact", number=8, completion_critical=True),
|
||||
"The Moebius Factor": MissionInfo(10, 9, [9], "Artifact", number=11, completion_critical=True),
|
||||
|
||||
@@ -43,6 +43,7 @@ class SC2WoLWorld(World):
|
||||
locked_locations: typing.List[str]
|
||||
location_cache: typing.List[Location]
|
||||
mission_req_table = {}
|
||||
required_client_version = 0, 3, 5
|
||||
|
||||
def __init__(self, world: MultiWorld, player: int):
|
||||
super(SC2WoLWorld, self).__init__(world, player)
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
from worlds.sm.variaRandomizer.rando.Items import ItemManager
|
||||
|
||||
items_start_id = 83000
|
||||
|
||||
def gen_special_id():
|
||||
special_id_value_start = 32
|
||||
while True:
|
||||
yield special_id_value_start
|
||||
special_id_value_start += 1
|
||||
|
||||
gen_run = gen_special_id()
|
||||
|
||||
lookup_id_to_name = dict((items_start_id + (value.Id if value.Id != None else next(gen_run)), value.Name) for key, value in ItemManager.Items.items())
|
||||
lookup_name_to_id = {item_name: item_id for item_id, item_name in lookup_id_to_name.items()}
|
||||
@@ -1,14 +0,0 @@
|
||||
from worlds.sm.variaRandomizer.graph.location import locationsDict
|
||||
|
||||
locations_start_id = 82000
|
||||
|
||||
def gen_boss_id():
|
||||
boss_id_value_start = 256
|
||||
while True:
|
||||
yield boss_id_value_start
|
||||
boss_id_value_start += 1
|
||||
|
||||
gen_run = gen_boss_id()
|
||||
|
||||
lookup_id_to_name = dict((locations_start_id + (value.Id if value.Id != None else next(gen_run)), key) for key, value in locationsDict.items())
|
||||
lookup_name_to_id = {location_name: location_id for location_id, location_name in lookup_id_to_name.items()}
|
||||
@@ -122,8 +122,6 @@ class AreaRandomization(Choice):
|
||||
option_off = 0
|
||||
option_light = 1
|
||||
option_on = 2
|
||||
alias_false = 0
|
||||
alias_true = 2
|
||||
default = 0
|
||||
|
||||
class AreaLayout(Toggle):
|
||||
|
||||
@@ -11,8 +11,6 @@ from worlds.sm.variaRandomizer.graph.graph_utils import GraphUtils
|
||||
|
||||
logger = logging.getLogger("Super Metroid")
|
||||
|
||||
from .Locations import lookup_name_to_id as locations_lookup_name_to_id
|
||||
from .Items import lookup_name_to_id as items_lookup_name_to_id
|
||||
from .Regions import create_regions
|
||||
from .Rules import set_rules, add_entrance_rule
|
||||
from .Options import sm_options
|
||||
@@ -68,6 +66,8 @@ class SMWeb(WebWorld):
|
||||
["Farrak Kilhn"]
|
||||
)]
|
||||
|
||||
locations_start_id = 82000
|
||||
items_start_id = 83000
|
||||
|
||||
class SMWorld(World):
|
||||
"""
|
||||
@@ -78,12 +78,11 @@ class SMWorld(World):
|
||||
|
||||
game: str = "Super Metroid"
|
||||
topology_present = True
|
||||
data_version = 1
|
||||
data_version = 2
|
||||
option_definitions = sm_options
|
||||
item_names: Set[str] = frozenset(items_lookup_name_to_id)
|
||||
location_names: Set[str] = frozenset(locations_lookup_name_to_id)
|
||||
item_name_to_id = items_lookup_name_to_id
|
||||
location_name_to_id = locations_lookup_name_to_id
|
||||
|
||||
item_name_to_id = {value.Name: items_start_id + value.Id for key, value in ItemManager.Items.items() if value.Id != None}
|
||||
location_name_to_id = {key: locations_start_id + value.Id for key, value in locationsDict.items() if value.Id != None}
|
||||
web = SMWeb()
|
||||
|
||||
remote_items: bool = False
|
||||
@@ -661,8 +660,7 @@ class SMWorld(World):
|
||||
loc.address = loc.item.code = None
|
||||
|
||||
@classmethod
|
||||
def stage_fill_hook(cls, world, progitempool, nonexcludeditempool, localrestitempool, nonlocalrestitempool,
|
||||
restitempool, fill_locations):
|
||||
def stage_fill_hook(cls, world, progitempool, usefulitempool, filleritempool, fill_locations):
|
||||
if world.get_game_players("Super Metroid"):
|
||||
progitempool.sort(
|
||||
key=lambda item: 1 if (item.name == 'Morph Ball') else 0)
|
||||
@@ -701,8 +699,8 @@ class SMWorld(World):
|
||||
dest.Name) for src, dest in self.variaRando.randoExec.areaGraph.InterAreaTransitions if src.Boss]))
|
||||
|
||||
def create_locations(self, player: int):
|
||||
for name, id in locations_lookup_name_to_id.items():
|
||||
self.locations[name] = SMLocation(player, name, id)
|
||||
for name in locationsDict:
|
||||
self.locations[name] = SMLocation(player, name, self.location_name_to_id.get(name, None))
|
||||
|
||||
|
||||
def create_region(self, world: MultiWorld, player: int, name: str, locations=None, exits=None):
|
||||
|
||||
Binary file not shown.
@@ -2,14 +2,14 @@
|
||||
; generated by asar
|
||||
|
||||
[labels]
|
||||
B8:80C1 :neg_1_1
|
||||
B8:83C1 :neg_1_1
|
||||
85:B9B4 :neg_1_2
|
||||
85:B9E6 :neg_1_3
|
||||
B8:C81F :neg_1_4
|
||||
B8:C831 :neg_1_5
|
||||
B8:C843 :neg_1_6
|
||||
B8:800C :pos_1_0
|
||||
B8:82D7 :pos_1_1
|
||||
B8:830C :pos_1_0
|
||||
B8:85D7 :pos_1_1
|
||||
84:FA6B :pos_1_2
|
||||
84:FA75 :pos_1_3
|
||||
B8:C862 :pos_1_4
|
||||
@@ -20,7 +20,7 @@ B8:C87C :pos_1_6
|
||||
85:990F CLIPLEN_end
|
||||
85:990C CLIPLEN_no_multi
|
||||
85:FF1D CLIPSET
|
||||
B8:81E8 COLLECTTANK
|
||||
B8:84E8 COLLECTTANK
|
||||
85:FF45 MISCFX
|
||||
84:8BF2 NORMAL
|
||||
85:FF4E SETFX
|
||||
@@ -38,11 +38,11 @@ CE:FF00 config_multiworld
|
||||
CE:FF08 config_player_id
|
||||
CE:FF06 config_remote_items
|
||||
CE:FF02 config_sprite
|
||||
B8:8119 copy_config_to_sram
|
||||
B8:80FD copy_memory
|
||||
B8:8117 copy_memory_done
|
||||
B8:8109 copy_memory_even
|
||||
B8:810F copy_memory_loop
|
||||
B8:8419 copy_config_to_sram
|
||||
B8:83FD copy_memory
|
||||
B8:8417 copy_memory_done
|
||||
B8:8409 copy_memory_even
|
||||
B8:840F copy_memory_loop
|
||||
84:F894 h_item
|
||||
84:F8AD i_chozo_item
|
||||
84:F8B4 i_hidden_item
|
||||
@@ -51,11 +51,11 @@ B8:885C i_item_setup_shared
|
||||
B8:8878 i_item_setup_shared_all_items
|
||||
B8:8883 i_item_setup_shared_alwaysloaded
|
||||
84:FA79 i_live_pickup
|
||||
B8:8278 i_live_pickup_multiworld
|
||||
B8:82BD i_live_pickup_multiworld_end
|
||||
B8:8294 i_live_pickup_multiworld_local_item_or_offworld
|
||||
B8:82A9 i_live_pickup_multiworld_own_item
|
||||
B8:82B5 i_live_pickup_multiworld_own_item1
|
||||
B8:8578 i_live_pickup_multiworld
|
||||
B8:85BD i_live_pickup_multiworld_end
|
||||
B8:8594 i_live_pickup_multiworld_local_item_or_offworld
|
||||
B8:85A9 i_live_pickup_multiworld_own_item
|
||||
B8:85B5 i_live_pickup_multiworld_own_item1
|
||||
84:FA1E i_load_custom_graphics
|
||||
84:FA39 i_load_custom_graphics_all_items
|
||||
84:FA49 i_load_custom_graphics_alwaysloaded
|
||||
@@ -90,27 +90,27 @@ B8:82B5 i_live_pickup_multiworld_own_item1
|
||||
85:B9CA message_write_placeholders_loop
|
||||
85:B9DC message_write_placeholders_notfound
|
||||
85:B9DF message_write_placeholders_value_ok
|
||||
B8:818B mw_display_item_sent
|
||||
B8:81F8 mw_handle_queue
|
||||
B8:8271 mw_handle_queue_end
|
||||
B8:81FA mw_handle_queue_loop
|
||||
B8:824A mw_handle_queue_new_remote_item
|
||||
B8:8266 mw_handle_queue_next
|
||||
B8:825C mw_handle_queue_perform_receive
|
||||
B8:82C1 mw_hook_main_game
|
||||
B8:8011 mw_init
|
||||
B8:8066 mw_init_continuereset
|
||||
B8:80EA mw_init_end
|
||||
B8:8000 mw_init_memory
|
||||
B8:803B mw_init_reset_sram
|
||||
B8:8051 mw_init_smstringdata
|
||||
B8:8174 mw_load_sram
|
||||
B8:8182 mw_load_sram_done
|
||||
B8:8185 mw_load_sram_setnewgame
|
||||
B8:81A9 mw_receive_item
|
||||
B8:81E1 mw_receive_item_end
|
||||
B8:8169 mw_save_sram
|
||||
B8:8142 mw_write_message
|
||||
B8:848B mw_display_item_sent
|
||||
B8:84F8 mw_handle_queue
|
||||
B8:8571 mw_handle_queue_end
|
||||
B8:84FA mw_handle_queue_loop
|
||||
B8:854A mw_handle_queue_new_remote_item
|
||||
B8:8566 mw_handle_queue_next
|
||||
B8:855C mw_handle_queue_perform_receive
|
||||
B8:85C1 mw_hook_main_game
|
||||
B8:8311 mw_init
|
||||
B8:8366 mw_init_continuereset
|
||||
B8:83EA mw_init_end
|
||||
B8:8300 mw_init_memory
|
||||
B8:833B mw_init_reset_sram
|
||||
B8:8351 mw_init_smstringdata
|
||||
B8:8474 mw_load_sram
|
||||
B8:8482 mw_load_sram_done
|
||||
B8:8485 mw_load_sram_setnewgame
|
||||
B8:84A9 mw_receive_item
|
||||
B8:84E1 mw_receive_item_end
|
||||
B8:8469 mw_save_sram
|
||||
B8:8442 mw_write_message
|
||||
84:F888 nonprog_item_eight_palette_indices
|
||||
89:9200 offworld_graphics_data_item
|
||||
89:9100 offworld_graphics_data_progression_item
|
||||
@@ -135,7 +135,7 @@ B8:8142 mw_write_message
|
||||
84:F96E p_visible_item_end
|
||||
84:F95B p_visible_item_loop
|
||||
84:F967 p_visible_item_trigger
|
||||
B8:82D8 patch_load_multiworld
|
||||
B8:85D8 patch_load_multiworld
|
||||
84:FA7E perform_item_pickup
|
||||
84:F886 plm_graphics_entry_offworld_item
|
||||
84:F87C plm_graphics_entry_offworld_progression_item
|
||||
@@ -154,19 +154,19 @@ B8:C808 start_item_data_minor
|
||||
B8:C818 start_item_data_reserve
|
||||
B8:C856 update_graphic
|
||||
84:F890 v_item
|
||||
B8:80EF write_repeated_memory
|
||||
B8:80F4 write_repeated_memory_loop
|
||||
B8:83EF write_repeated_memory
|
||||
B8:83F4 write_repeated_memory_loop
|
||||
|
||||
[source files]
|
||||
0000 e25029c5 main.asm
|
||||
0000 f30c3fdc main.asm
|
||||
0001 06780555 ../common/nofanfare.asm
|
||||
0002 4f9a780e ../common/multiworld.asm
|
||||
0003 613d24e1 ../common/itemextras.asm
|
||||
0003 f7e9db95 ../common/itemextras.asm
|
||||
0004 d6616c0c ../common/items.asm
|
||||
0005 440b54fe ../common/startitem.asm
|
||||
0005 dbfcb38d ../common/startitem.asm
|
||||
|
||||
[rom checksum]
|
||||
ad81eda1
|
||||
b526d5c7
|
||||
|
||||
[addr-to-line mapping]
|
||||
ff:ffff 0000:00000001
|
||||
@@ -216,316 +216,316 @@ ff:ffff 0000:00000001
|
||||
84:8bf2 0001:00000152
|
||||
84:8bf6 0001:00000153
|
||||
84:8bf7 0001:00000153
|
||||
b8:8000 0002:0000005a
|
||||
b8:8002 0002:0000005b
|
||||
b8:8006 0002:0000005c
|
||||
b8:8008 0002:0000005d
|
||||
b8:800c 0002:00000061
|
||||
b8:800e 0002:00000062
|
||||
b8:8010 0002:00000063
|
||||
b8:8011 0002:00000066
|
||||
b8:8012 0002:00000066
|
||||
b8:8013 0002:00000066
|
||||
b8:8014 0002:00000066
|
||||
b8:8015 0000:00000013
|
||||
b8:8017 0002:0000006a
|
||||
b8:801b 0002:0000006b
|
||||
b8:801e 0002:0000006c
|
||||
b8:8020 0002:0000006d
|
||||
b8:8024 0002:0000006e
|
||||
b8:8028 0002:0000006f
|
||||
b8:802a 0002:00000070
|
||||
b8:802e 0002:00000071
|
||||
b8:8032 0002:00000072
|
||||
b8:8034 0002:00000074
|
||||
b8:8038 0002:00000075
|
||||
b8:803b 0002:00000078
|
||||
b8:803c 0002:00000079
|
||||
b8:803f 0002:0000007a
|
||||
b8:8042 0002:0000007b
|
||||
b8:8045 0002:0000007c
|
||||
b8:8048 0002:0000007d
|
||||
b8:8049 0002:0000007e
|
||||
b8:804a 0002:0000007f
|
||||
b8:804e 0002:00000080
|
||||
b8:804f 0002:00000082
|
||||
b8:8066 0002:00000086
|
||||
b8:8068 0002:00000087
|
||||
b8:8069 0002:00000088
|
||||
b8:806a 0002:00000089
|
||||
b8:806c 0002:0000008a
|
||||
b8:806e 0002:0000008b
|
||||
b8:8070 0002:0000008c
|
||||
b8:8072 0002:0000008d
|
||||
b8:8075 0002:0000008e
|
||||
b8:8077 0002:0000008f
|
||||
b8:807a 0002:00000090
|
||||
b8:807d 0002:00000091
|
||||
b8:807f 0002:00000092
|
||||
b8:8083 0002:00000094
|
||||
b8:8085 0002:00000095
|
||||
b8:8087 0002:00000096
|
||||
b8:8089 0002:00000097
|
||||
b8:808b 0002:00000098
|
||||
b8:808d 0002:00000099
|
||||
b8:808f 0002:0000009a
|
||||
b8:8092 0002:0000009b
|
||||
b8:8094 0002:0000009c
|
||||
b8:8097 0002:0000009d
|
||||
b8:809a 0002:0000009e
|
||||
b8:809c 0002:0000009f
|
||||
b8:80a0 0002:000000a1
|
||||
b8:80a3 0002:000000a2
|
||||
b8:80a7 0002:000000a3
|
||||
b8:80ab 0002:000000a4
|
||||
b8:80af 0002:000000a5
|
||||
b8:80b3 0002:000000a6
|
||||
b8:80b7 0002:000000a8
|
||||
b8:80bb 0002:000000b0
|
||||
b8:80be 0002:000000b1
|
||||
b8:80c1 0002:000000b3
|
||||
b8:80c2 0002:000000b4
|
||||
b8:80c3 0002:000000b5
|
||||
b8:80c7 0002:000000b6
|
||||
b8:80cb 0002:000000b7
|
||||
b8:80cd 0002:000000c4
|
||||
b8:80d1 0002:000000c5
|
||||
b8:80d4 0002:000000c6
|
||||
b8:80d6 0002:000000c7
|
||||
b8:80da 0002:000000c8
|
||||
b8:80dd 0002:000000c9
|
||||
b8:80df 0002:000000ce
|
||||
b8:80e2 0002:000000cf
|
||||
b8:80e6 0002:000000d0
|
||||
b8:80ea 0002:000000d3
|
||||
b8:80eb 0002:000000d3
|
||||
b8:80ec 0002:000000d3
|
||||
b8:80ed 0002:000000d3
|
||||
b8:80ee 0002:000000d4
|
||||
b8:80ef 0002:000000db
|
||||
b8:80f0 0002:000000dc
|
||||
b8:80f1 0002:000000dd
|
||||
b8:80f2 0002:000000de
|
||||
b8:80f3 0002:000000df
|
||||
b8:80f4 0002:000000e1
|
||||
b8:80f7 0002:000000e2
|
||||
b8:80f8 0002:000000e3
|
||||
b8:80f9 0002:000000e4
|
||||
b8:80fa 0002:000000e5
|
||||
b8:80fc 0002:000000e7
|
||||
b8:80fd 0002:000000ee
|
||||
b8:80fe 0002:000000ef
|
||||
b8:80ff 0002:000000f0
|
||||
b8:8100 0002:000000f1
|
||||
b8:8102 0002:000000f3
|
||||
b8:8104 0002:000000f4
|
||||
b8:8105 0002:000000f5
|
||||
b8:8107 0002:000000f6
|
||||
b8:8109 0002:000000f8
|
||||
b8:810b 0002:000000f9
|
||||
b8:810c 0002:000000fa
|
||||
b8:810d 0002:000000fb
|
||||
b8:810f 0002:000000fd
|
||||
b8:8111 0002:000000fe
|
||||
b8:8113 0002:000000ff
|
||||
b8:8114 0002:00000100
|
||||
b8:8115 0002:00000101
|
||||
b8:8117 0002:00000103
|
||||
b8:8118 0002:00000104
|
||||
b8:8119 0002:00000108
|
||||
b8:811d 0002:00000109
|
||||
b8:8121 0002:0000010a
|
||||
b8:8125 0002:0000010b
|
||||
b8:8129 0002:0000010c
|
||||
b8:812d 0002:0000010d
|
||||
b8:8131 0002:0000010e
|
||||
b8:8135 0002:0000010f
|
||||
b8:8139 0002:00000110
|
||||
b8:813d 0002:00000111
|
||||
b8:8141 0002:00000112
|
||||
b8:8142 0002:00000118
|
||||
b8:8143 0002:00000118
|
||||
b8:8144 0002:00000119
|
||||
b8:8145 0002:00000119
|
||||
b8:8146 0002:0000011a
|
||||
b8:814a 0002:0000011b
|
||||
b8:814d 0002:0000011b
|
||||
b8:814e 0002:0000011c
|
||||
b8:814f 0002:0000011d
|
||||
b8:8153 0002:0000011e
|
||||
b8:8154 0002:0000011f
|
||||
b8:8158 0002:00000120
|
||||
b8:8159 0002:00000121
|
||||
b8:815d 0002:00000123
|
||||
b8:8161 0002:00000124
|
||||
b8:8162 0002:00000125
|
||||
b8:8166 0002:00000126
|
||||
b8:8167 0002:00000126
|
||||
b8:8168 0002:00000127
|
||||
b8:8169 0002:0000012c
|
||||
b8:816a 0002:0000012c
|
||||
b8:816b 0000:00000013
|
||||
b8:816d 0002:0000012f
|
||||
b8:816e 0002:0000012f
|
||||
b8:816f 0002:00000131
|
||||
b8:8170 0002:00000132
|
||||
b8:8173 0002:00000133
|
||||
b8:8174 0002:00000138
|
||||
b8:8175 0002:00000138
|
||||
b8:8176 0000:00000013
|
||||
b8:8178 0002:0000013a
|
||||
b8:817c 0002:0000013b
|
||||
b8:8180 0002:0000013c
|
||||
b8:8182 0002:0000013e
|
||||
b8:8183 0002:0000013e
|
||||
b8:8184 0002:0000013f
|
||||
b8:8185 0002:00000147
|
||||
b8:8189 0002:00000148
|
||||
b8:818b 0002:0000014e
|
||||
b8:818d 0002:0000014f
|
||||
b8:818f 0002:00000152
|
||||
b8:8192 0002:00000153
|
||||
b8:8194 0002:00000154
|
||||
b8:8197 0002:00000155
|
||||
b8:8199 0002:00000156
|
||||
b8:819c 0002:00000157
|
||||
b8:81a0 0002:00000158
|
||||
b8:81a2 0002:00000159
|
||||
b8:81a4 0002:0000015a
|
||||
b8:81a6 0002:0000015b
|
||||
b8:81a8 0002:0000015c
|
||||
b8:81a9 0002:00000160
|
||||
b8:81aa 0002:00000160
|
||||
b8:81ab 0002:00000161
|
||||
b8:81ae 0002:00000162
|
||||
b8:81b0 0002:00000163
|
||||
b8:81b3 0002:00000164
|
||||
b8:81b5 0002:00000165
|
||||
b8:81b6 0002:00000166
|
||||
b8:81b7 0002:00000168
|
||||
b8:81ba 0002:00000169
|
||||
b8:81bc 0002:0000016a
|
||||
b8:81bf 0002:0000016b
|
||||
b8:81c0 0002:0000016c
|
||||
b8:81c3 0002:0000016d
|
||||
b8:81c4 0002:0000016d
|
||||
b8:81c5 0002:0000016e
|
||||
b8:81c9 0002:0000016f
|
||||
b8:81ca 0002:00000170
|
||||
b8:81cd 0002:00000171
|
||||
b8:81d1 0002:00000172
|
||||
b8:81d3 0002:00000174
|
||||
b8:81d6 0002:00000175
|
||||
b8:81d8 0002:00000176
|
||||
b8:81db 0002:00000177
|
||||
b8:81dd 0002:00000179
|
||||
b8:81e1 0002:0000017b
|
||||
b8:81e3 0002:0000017c
|
||||
b8:81e5 0002:0000017d
|
||||
b8:81e6 0002:0000017d
|
||||
b8:81e7 0002:0000017e
|
||||
b8:81e8 0002:00000189
|
||||
b8:81e9 0002:0000018a
|
||||
b8:81ed 0002:0000018b
|
||||
b8:81ee 0002:0000018c
|
||||
b8:81f2 0002:0000018d
|
||||
b8:81f3 0002:0000018f
|
||||
b8:81f7 0002:00000190
|
||||
b8:81f8 0002:000001c2
|
||||
b8:81f9 0002:000001c2
|
||||
b8:81fa 0002:000001c5
|
||||
b8:81fe 0002:000001c6
|
||||
b8:8202 0002:000001c7
|
||||
b8:8204 0002:000001c9
|
||||
b8:8206 0002:000001c9
|
||||
b8:8207 0002:000001cc
|
||||
b8:820b 0002:000001cd
|
||||
b8:820d 0002:000001ce
|
||||
b8:8211 0002:000001cf
|
||||
b8:8213 0002:000001d0
|
||||
b8:8217 0002:000001d1
|
||||
b8:821a 0002:000001d2
|
||||
b8:821c 0002:000001d3
|
||||
b8:821e 0002:000001d4
|
||||
b8:8222 0002:000001d5
|
||||
b8:8224 0002:000001d6
|
||||
b8:8226 0002:000001d7
|
||||
b8:8229 0002:000001d8
|
||||
b8:822c 0002:000001d9
|
||||
b8:822e 0002:000001da
|
||||
b8:8236 0002:000001de
|
||||
b8:8237 0002:000001df
|
||||
b8:8238 0002:000001e0
|
||||
b8:823c 0002:000001e3
|
||||
b8:8240 0002:000001e4
|
||||
b8:8244 0002:000001e5
|
||||
b8:8246 0002:000001e7
|
||||
b8:8247 0002:000001e8
|
||||
b8:8248 0002:000001e9
|
||||
b8:824a 0002:000001ee
|
||||
b8:824b 0002:000001ef
|
||||
b8:824f 0002:000001f2
|
||||
b8:8253 0002:000001f3
|
||||
b8:8257 0002:000001f4
|
||||
b8:825b 0002:000001f5
|
||||
b8:825c 0002:000001f9
|
||||
b8:825e 0002:000001fa
|
||||
b8:8261 0002:000001fb
|
||||
b8:8263 0002:000001fc
|
||||
b8:8266 0002:000001ff
|
||||
b8:826a 0002:00000200
|
||||
b8:826b 0002:00000201
|
||||
b8:826f 0002:00000203
|
||||
b8:8271 0002:00000206
|
||||
b8:8273 0002:00000207
|
||||
b8:8275 0002:00000208
|
||||
b8:8276 0002:00000208
|
||||
b8:8277 0002:00000209
|
||||
b8:8278 0002:0000020d
|
||||
b8:8279 0002:0000020d
|
||||
b8:827a 0002:0000020d
|
||||
b8:827b 0002:0000020e
|
||||
b8:827f 0002:0000020f
|
||||
b8:8282 0002:0000020f
|
||||
b8:8283 0002:00000211
|
||||
b8:8287 0002:00000212
|
||||
b8:8288 0002:00000213
|
||||
b8:828c 0002:00000214
|
||||
b8:828f 0002:00000215
|
||||
b8:8291 0002:00000217
|
||||
b8:8294 0002:0000021a
|
||||
b8:8298 0002:0000021b
|
||||
b8:829c 0002:0000021c
|
||||
b8:829e 0002:0000021e
|
||||
b8:82a2 0002:0000021f
|
||||
b8:82a3 0002:00000221
|
||||
b8:82a7 0002:00000222
|
||||
b8:82a9 0002:00000225
|
||||
b8:82ad 0002:00000226
|
||||
b8:82b0 0002:00000227
|
||||
b8:82b2 0002:00000228
|
||||
b8:82b5 0002:0000022b
|
||||
b8:82b6 0002:0000022c
|
||||
b8:82b7 0002:0000022d
|
||||
b8:82bb 0002:0000022e
|
||||
b8:82bd 0002:00000231
|
||||
b8:82be 0002:00000231
|
||||
b8:82bf 0002:00000231
|
||||
b8:82c0 0002:00000232
|
||||
b8:82c1 0002:00000236
|
||||
b8:82c5 0002:00000237
|
||||
b8:82c9 0002:00000238
|
||||
b8:82cb 0002:00000239
|
||||
b8:82cf 0002:0000023a
|
||||
b8:82d2 0002:0000023b
|
||||
b8:82d4 0002:0000023c
|
||||
b8:82d7 0002:0000023e
|
||||
b8:82d8 0002:00000241
|
||||
b8:82dc 0002:00000243
|
||||
b8:82dd 0002:00000244
|
||||
b8:82de 0002:00000245
|
||||
b8:82df 0002:00000246
|
||||
b8:82e0 0002:00000247
|
||||
b8:8300 0002:0000005a
|
||||
b8:8302 0002:0000005b
|
||||
b8:8306 0002:0000005c
|
||||
b8:8308 0002:0000005d
|
||||
b8:830c 0002:00000061
|
||||
b8:830e 0002:00000062
|
||||
b8:8310 0002:00000063
|
||||
b8:8311 0002:00000066
|
||||
b8:8312 0002:00000066
|
||||
b8:8313 0002:00000066
|
||||
b8:8314 0002:00000066
|
||||
b8:8315 0000:00000013
|
||||
b8:8317 0002:0000006a
|
||||
b8:831b 0002:0000006b
|
||||
b8:831e 0002:0000006c
|
||||
b8:8320 0002:0000006d
|
||||
b8:8324 0002:0000006e
|
||||
b8:8328 0002:0000006f
|
||||
b8:832a 0002:00000070
|
||||
b8:832e 0002:00000071
|
||||
b8:8332 0002:00000072
|
||||
b8:8334 0002:00000074
|
||||
b8:8338 0002:00000075
|
||||
b8:833b 0002:00000078
|
||||
b8:833c 0002:00000079
|
||||
b8:833f 0002:0000007a
|
||||
b8:8342 0002:0000007b
|
||||
b8:8345 0002:0000007c
|
||||
b8:8348 0002:0000007d
|
||||
b8:8349 0002:0000007e
|
||||
b8:834a 0002:0000007f
|
||||
b8:834e 0002:00000080
|
||||
b8:834f 0002:00000082
|
||||
b8:8366 0002:00000086
|
||||
b8:8368 0002:00000087
|
||||
b8:8369 0002:00000088
|
||||
b8:836a 0002:00000089
|
||||
b8:836c 0002:0000008a
|
||||
b8:836e 0002:0000008b
|
||||
b8:8370 0002:0000008c
|
||||
b8:8372 0002:0000008d
|
||||
b8:8375 0002:0000008e
|
||||
b8:8377 0002:0000008f
|
||||
b8:837a 0002:00000090
|
||||
b8:837d 0002:00000091
|
||||
b8:837f 0002:00000092
|
||||
b8:8383 0002:00000094
|
||||
b8:8385 0002:00000095
|
||||
b8:8387 0002:00000096
|
||||
b8:8389 0002:00000097
|
||||
b8:838b 0002:00000098
|
||||
b8:838d 0002:00000099
|
||||
b8:838f 0002:0000009a
|
||||
b8:8392 0002:0000009b
|
||||
b8:8394 0002:0000009c
|
||||
b8:8397 0002:0000009d
|
||||
b8:839a 0002:0000009e
|
||||
b8:839c 0002:0000009f
|
||||
b8:83a0 0002:000000a1
|
||||
b8:83a3 0002:000000a2
|
||||
b8:83a7 0002:000000a3
|
||||
b8:83ab 0002:000000a4
|
||||
b8:83af 0002:000000a5
|
||||
b8:83b3 0002:000000a6
|
||||
b8:83b7 0002:000000a8
|
||||
b8:83bb 0002:000000b0
|
||||
b8:83be 0002:000000b1
|
||||
b8:83c1 0002:000000b3
|
||||
b8:83c2 0002:000000b4
|
||||
b8:83c3 0002:000000b5
|
||||
b8:83c7 0002:000000b6
|
||||
b8:83cb 0002:000000b7
|
||||
b8:83cd 0002:000000c4
|
||||
b8:83d1 0002:000000c5
|
||||
b8:83d4 0002:000000c6
|
||||
b8:83d6 0002:000000c7
|
||||
b8:83da 0002:000000c8
|
||||
b8:83dd 0002:000000c9
|
||||
b8:83df 0002:000000ce
|
||||
b8:83e2 0002:000000cf
|
||||
b8:83e6 0002:000000d0
|
||||
b8:83ea 0002:000000d3
|
||||
b8:83eb 0002:000000d3
|
||||
b8:83ec 0002:000000d3
|
||||
b8:83ed 0002:000000d3
|
||||
b8:83ee 0002:000000d4
|
||||
b8:83ef 0002:000000db
|
||||
b8:83f0 0002:000000dc
|
||||
b8:83f1 0002:000000dd
|
||||
b8:83f2 0002:000000de
|
||||
b8:83f3 0002:000000df
|
||||
b8:83f4 0002:000000e1
|
||||
b8:83f7 0002:000000e2
|
||||
b8:83f8 0002:000000e3
|
||||
b8:83f9 0002:000000e4
|
||||
b8:83fa 0002:000000e5
|
||||
b8:83fc 0002:000000e7
|
||||
b8:83fd 0002:000000ee
|
||||
b8:83fe 0002:000000ef
|
||||
b8:83ff 0002:000000f0
|
||||
b8:8400 0002:000000f1
|
||||
b8:8402 0002:000000f3
|
||||
b8:8404 0002:000000f4
|
||||
b8:8405 0002:000000f5
|
||||
b8:8407 0002:000000f6
|
||||
b8:8409 0002:000000f8
|
||||
b8:840b 0002:000000f9
|
||||
b8:840c 0002:000000fa
|
||||
b8:840d 0002:000000fb
|
||||
b8:840f 0002:000000fd
|
||||
b8:8411 0002:000000fe
|
||||
b8:8413 0002:000000ff
|
||||
b8:8414 0002:00000100
|
||||
b8:8415 0002:00000101
|
||||
b8:8417 0002:00000103
|
||||
b8:8418 0002:00000104
|
||||
b8:8419 0002:00000108
|
||||
b8:841d 0002:00000109
|
||||
b8:8421 0002:0000010a
|
||||
b8:8425 0002:0000010b
|
||||
b8:8429 0002:0000010c
|
||||
b8:842d 0002:0000010d
|
||||
b8:8431 0002:0000010e
|
||||
b8:8435 0002:0000010f
|
||||
b8:8439 0002:00000110
|
||||
b8:843d 0002:00000111
|
||||
b8:8441 0002:00000112
|
||||
b8:8442 0002:00000118
|
||||
b8:8443 0002:00000118
|
||||
b8:8444 0002:00000119
|
||||
b8:8445 0002:00000119
|
||||
b8:8446 0002:0000011a
|
||||
b8:844a 0002:0000011b
|
||||
b8:844d 0002:0000011b
|
||||
b8:844e 0002:0000011c
|
||||
b8:844f 0002:0000011d
|
||||
b8:8453 0002:0000011e
|
||||
b8:8454 0002:0000011f
|
||||
b8:8458 0002:00000120
|
||||
b8:8459 0002:00000121
|
||||
b8:845d 0002:00000123
|
||||
b8:8461 0002:00000124
|
||||
b8:8462 0002:00000125
|
||||
b8:8466 0002:00000126
|
||||
b8:8467 0002:00000126
|
||||
b8:8468 0002:00000127
|
||||
b8:8469 0002:0000012c
|
||||
b8:846a 0002:0000012c
|
||||
b8:846b 0000:00000013
|
||||
b8:846d 0002:0000012f
|
||||
b8:846e 0002:0000012f
|
||||
b8:846f 0002:00000131
|
||||
b8:8470 0002:00000132
|
||||
b8:8473 0002:00000133
|
||||
b8:8474 0002:00000138
|
||||
b8:8475 0002:00000138
|
||||
b8:8476 0000:00000013
|
||||
b8:8478 0002:0000013a
|
||||
b8:847c 0002:0000013b
|
||||
b8:8480 0002:0000013c
|
||||
b8:8482 0002:0000013e
|
||||
b8:8483 0002:0000013e
|
||||
b8:8484 0002:0000013f
|
||||
b8:8485 0002:00000147
|
||||
b8:8489 0002:00000148
|
||||
b8:848b 0002:0000014e
|
||||
b8:848d 0002:0000014f
|
||||
b8:848f 0002:00000152
|
||||
b8:8492 0002:00000153
|
||||
b8:8494 0002:00000154
|
||||
b8:8497 0002:00000155
|
||||
b8:8499 0002:00000156
|
||||
b8:849c 0002:00000157
|
||||
b8:84a0 0002:00000158
|
||||
b8:84a2 0002:00000159
|
||||
b8:84a4 0002:0000015a
|
||||
b8:84a6 0002:0000015b
|
||||
b8:84a8 0002:0000015c
|
||||
b8:84a9 0002:00000160
|
||||
b8:84aa 0002:00000160
|
||||
b8:84ab 0002:00000161
|
||||
b8:84ae 0002:00000162
|
||||
b8:84b0 0002:00000163
|
||||
b8:84b3 0002:00000164
|
||||
b8:84b5 0002:00000165
|
||||
b8:84b6 0002:00000166
|
||||
b8:84b7 0002:00000168
|
||||
b8:84ba 0002:00000169
|
||||
b8:84bc 0002:0000016a
|
||||
b8:84bf 0002:0000016b
|
||||
b8:84c0 0002:0000016c
|
||||
b8:84c3 0002:0000016d
|
||||
b8:84c4 0002:0000016d
|
||||
b8:84c5 0002:0000016e
|
||||
b8:84c9 0002:0000016f
|
||||
b8:84ca 0002:00000170
|
||||
b8:84cd 0002:00000171
|
||||
b8:84d1 0002:00000172
|
||||
b8:84d3 0002:00000174
|
||||
b8:84d6 0002:00000175
|
||||
b8:84d8 0002:00000176
|
||||
b8:84db 0002:00000177
|
||||
b8:84dd 0002:00000179
|
||||
b8:84e1 0002:0000017b
|
||||
b8:84e3 0002:0000017c
|
||||
b8:84e5 0002:0000017d
|
||||
b8:84e6 0002:0000017d
|
||||
b8:84e7 0002:0000017e
|
||||
b8:84e8 0002:00000189
|
||||
b8:84e9 0002:0000018a
|
||||
b8:84ed 0002:0000018b
|
||||
b8:84ee 0002:0000018c
|
||||
b8:84f2 0002:0000018d
|
||||
b8:84f3 0002:0000018f
|
||||
b8:84f7 0002:00000190
|
||||
b8:84f8 0002:000001c2
|
||||
b8:84f9 0002:000001c2
|
||||
b8:84fa 0002:000001c5
|
||||
b8:84fe 0002:000001c6
|
||||
b8:8502 0002:000001c7
|
||||
b8:8504 0002:000001c9
|
||||
b8:8506 0002:000001c9
|
||||
b8:8507 0002:000001cc
|
||||
b8:850b 0002:000001cd
|
||||
b8:850d 0002:000001ce
|
||||
b8:8511 0002:000001cf
|
||||
b8:8513 0002:000001d0
|
||||
b8:8517 0002:000001d1
|
||||
b8:851a 0002:000001d2
|
||||
b8:851c 0002:000001d3
|
||||
b8:851e 0002:000001d4
|
||||
b8:8522 0002:000001d5
|
||||
b8:8524 0002:000001d6
|
||||
b8:8526 0002:000001d7
|
||||
b8:8529 0002:000001d8
|
||||
b8:852c 0002:000001d9
|
||||
b8:852e 0002:000001da
|
||||
b8:8536 0002:000001de
|
||||
b8:8537 0002:000001df
|
||||
b8:8538 0002:000001e0
|
||||
b8:853c 0002:000001e3
|
||||
b8:8540 0002:000001e4
|
||||
b8:8544 0002:000001e5
|
||||
b8:8546 0002:000001e7
|
||||
b8:8547 0002:000001e8
|
||||
b8:8548 0002:000001e9
|
||||
b8:854a 0002:000001ee
|
||||
b8:854b 0002:000001ef
|
||||
b8:854f 0002:000001f2
|
||||
b8:8553 0002:000001f3
|
||||
b8:8557 0002:000001f4
|
||||
b8:855b 0002:000001f5
|
||||
b8:855c 0002:000001f9
|
||||
b8:855e 0002:000001fa
|
||||
b8:8561 0002:000001fb
|
||||
b8:8563 0002:000001fc
|
||||
b8:8566 0002:000001ff
|
||||
b8:856a 0002:00000200
|
||||
b8:856b 0002:00000201
|
||||
b8:856f 0002:00000203
|
||||
b8:8571 0002:00000206
|
||||
b8:8573 0002:00000207
|
||||
b8:8575 0002:00000208
|
||||
b8:8576 0002:00000208
|
||||
b8:8577 0002:00000209
|
||||
b8:8578 0002:0000020d
|
||||
b8:8579 0002:0000020d
|
||||
b8:857a 0002:0000020d
|
||||
b8:857b 0002:0000020e
|
||||
b8:857f 0002:0000020f
|
||||
b8:8582 0002:0000020f
|
||||
b8:8583 0002:00000211
|
||||
b8:8587 0002:00000212
|
||||
b8:8588 0002:00000213
|
||||
b8:858c 0002:00000214
|
||||
b8:858f 0002:00000215
|
||||
b8:8591 0002:00000217
|
||||
b8:8594 0002:0000021a
|
||||
b8:8598 0002:0000021b
|
||||
b8:859c 0002:0000021c
|
||||
b8:859e 0002:0000021e
|
||||
b8:85a2 0002:0000021f
|
||||
b8:85a3 0002:00000221
|
||||
b8:85a7 0002:00000222
|
||||
b8:85a9 0002:00000225
|
||||
b8:85ad 0002:00000226
|
||||
b8:85b0 0002:00000227
|
||||
b8:85b2 0002:00000228
|
||||
b8:85b5 0002:0000022b
|
||||
b8:85b6 0002:0000022c
|
||||
b8:85b7 0002:0000022d
|
||||
b8:85bb 0002:0000022e
|
||||
b8:85bd 0002:00000231
|
||||
b8:85be 0002:00000231
|
||||
b8:85bf 0002:00000231
|
||||
b8:85c0 0002:00000232
|
||||
b8:85c1 0002:00000236
|
||||
b8:85c5 0002:00000237
|
||||
b8:85c9 0002:00000238
|
||||
b8:85cb 0002:00000239
|
||||
b8:85cf 0002:0000023a
|
||||
b8:85d2 0002:0000023b
|
||||
b8:85d4 0002:0000023c
|
||||
b8:85d7 0002:0000023e
|
||||
b8:85d8 0002:00000241
|
||||
b8:85dc 0002:00000243
|
||||
b8:85dd 0002:00000244
|
||||
b8:85de 0002:00000245
|
||||
b8:85df 0002:00000246
|
||||
b8:85e0 0002:00000247
|
||||
8b:914a 0002:0000024c
|
||||
81:80f7 0002:0000024f
|
||||
81:8027 0002:00000252
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"CLIPLEN_end": "85:990F",
|
||||
"CLIPLEN_no_multi": "85:990C",
|
||||
"CLIPSET": "85:FF1D",
|
||||
"COLLECTTANK": "B8:81E8",
|
||||
"COLLECTTANK": "B8:84E8",
|
||||
"MISCFX": "85:FF45",
|
||||
"NORMAL": "84:8BF2",
|
||||
"SETFX": "85:FF4E",
|
||||
@@ -22,11 +22,11 @@
|
||||
"config_player_id": "CE:FF08",
|
||||
"config_remote_items": "CE:FF06",
|
||||
"config_sprite": "CE:FF02",
|
||||
"copy_config_to_sram": "B8:8119",
|
||||
"copy_memory": "B8:80FD",
|
||||
"copy_memory_done": "B8:8117",
|
||||
"copy_memory_even": "B8:8109",
|
||||
"copy_memory_loop": "B8:810F",
|
||||
"copy_config_to_sram": "B8:8419",
|
||||
"copy_memory": "B8:83FD",
|
||||
"copy_memory_done": "B8:8417",
|
||||
"copy_memory_even": "B8:8409",
|
||||
"copy_memory_loop": "B8:840F",
|
||||
"h_item": "84:F894",
|
||||
"i_chozo_item": "84:F8AD",
|
||||
"i_hidden_item": "84:F8B4",
|
||||
@@ -35,11 +35,11 @@
|
||||
"i_item_setup_shared_all_items": "B8:8878",
|
||||
"i_item_setup_shared_alwaysloaded": "B8:8883",
|
||||
"i_live_pickup": "84:FA79",
|
||||
"i_live_pickup_multiworld": "B8:8278",
|
||||
"i_live_pickup_multiworld_end": "B8:82BD",
|
||||
"i_live_pickup_multiworld_local_item_or_offworld": "B8:8294",
|
||||
"i_live_pickup_multiworld_own_item": "B8:82A9",
|
||||
"i_live_pickup_multiworld_own_item1": "B8:82B5",
|
||||
"i_live_pickup_multiworld": "B8:8578",
|
||||
"i_live_pickup_multiworld_end": "B8:85BD",
|
||||
"i_live_pickup_multiworld_local_item_or_offworld": "B8:8594",
|
||||
"i_live_pickup_multiworld_own_item": "B8:85A9",
|
||||
"i_live_pickup_multiworld_own_item1": "B8:85B5",
|
||||
"i_load_custom_graphics": "84:FA1E",
|
||||
"i_load_custom_graphics_all_items": "84:FA39",
|
||||
"i_load_custom_graphics_alwaysloaded": "84:FA49",
|
||||
@@ -74,27 +74,27 @@
|
||||
"message_write_placeholders_loop": "85:B9CA",
|
||||
"message_write_placeholders_notfound": "85:B9DC",
|
||||
"message_write_placeholders_value_ok": "85:B9DF",
|
||||
"mw_display_item_sent": "B8:818B",
|
||||
"mw_handle_queue": "B8:81F8",
|
||||
"mw_handle_queue_end": "B8:8271",
|
||||
"mw_handle_queue_loop": "B8:81FA",
|
||||
"mw_handle_queue_new_remote_item": "B8:824A",
|
||||
"mw_handle_queue_next": "B8:8266",
|
||||
"mw_handle_queue_perform_receive": "B8:825C",
|
||||
"mw_hook_main_game": "B8:82C1",
|
||||
"mw_init": "B8:8011",
|
||||
"mw_init_continuereset": "B8:8066",
|
||||
"mw_init_end": "B8:80EA",
|
||||
"mw_init_memory": "B8:8000",
|
||||
"mw_init_reset_sram": "B8:803B",
|
||||
"mw_init_smstringdata": "B8:8051",
|
||||
"mw_load_sram": "B8:8174",
|
||||
"mw_load_sram_done": "B8:8182",
|
||||
"mw_load_sram_setnewgame": "B8:8185",
|
||||
"mw_receive_item": "B8:81A9",
|
||||
"mw_receive_item_end": "B8:81E1",
|
||||
"mw_save_sram": "B8:8169",
|
||||
"mw_write_message": "B8:8142",
|
||||
"mw_display_item_sent": "B8:848B",
|
||||
"mw_handle_queue": "B8:84F8",
|
||||
"mw_handle_queue_end": "B8:8571",
|
||||
"mw_handle_queue_loop": "B8:84FA",
|
||||
"mw_handle_queue_new_remote_item": "B8:854A",
|
||||
"mw_handle_queue_next": "B8:8566",
|
||||
"mw_handle_queue_perform_receive": "B8:855C",
|
||||
"mw_hook_main_game": "B8:85C1",
|
||||
"mw_init": "B8:8311",
|
||||
"mw_init_continuereset": "B8:8366",
|
||||
"mw_init_end": "B8:83EA",
|
||||
"mw_init_memory": "B8:8300",
|
||||
"mw_init_reset_sram": "B8:833B",
|
||||
"mw_init_smstringdata": "B8:8351",
|
||||
"mw_load_sram": "B8:8474",
|
||||
"mw_load_sram_done": "B8:8482",
|
||||
"mw_load_sram_setnewgame": "B8:8485",
|
||||
"mw_receive_item": "B8:84A9",
|
||||
"mw_receive_item_end": "B8:84E1",
|
||||
"mw_save_sram": "B8:8469",
|
||||
"mw_write_message": "B8:8442",
|
||||
"nonprog_item_eight_palette_indices": "84:F888",
|
||||
"offworld_graphics_data_item": "89:9200",
|
||||
"offworld_graphics_data_progression_item": "89:9100",
|
||||
@@ -119,7 +119,7 @@
|
||||
"p_visible_item_end": "84:F96E",
|
||||
"p_visible_item_loop": "84:F95B",
|
||||
"p_visible_item_trigger": "84:F967",
|
||||
"patch_load_multiworld": "B8:82D8",
|
||||
"patch_load_multiworld": "B8:85D8",
|
||||
"perform_item_pickup": "84:FA7E",
|
||||
"plm_graphics_entry_offworld_item": "84:F886",
|
||||
"plm_graphics_entry_offworld_progression_item": "84:F87C",
|
||||
@@ -138,8 +138,8 @@
|
||||
"start_item_data_reserve": "B8:C818",
|
||||
"update_graphic": "B8:C856",
|
||||
"v_item": "84:F890",
|
||||
"write_repeated_memory": "B8:80EF",
|
||||
"write_repeated_memory_loop": "B8:80F4",
|
||||
"write_repeated_memory": "B8:83EF",
|
||||
"write_repeated_memory_loop": "B8:83F4",
|
||||
"ITEM_RAM": "7E:09A2",
|
||||
"SRAM_MW_ITEMS_RECV": "70:2000",
|
||||
"SRAM_MW_ITEMS_RECV_WCOUNT": "70:2602",
|
||||
|
||||
@@ -294,7 +294,18 @@ accessPoints = [
|
||||
sm.canGetBackFromRidleyZone(),
|
||||
sm.canPassWastelandDessgeegas(),
|
||||
sm.canPassRedKiHunters())),
|
||||
'RidleyRoomOut': Cache.ldeco(lambda sm: sm.canHellRun(**Settings.hellRunsTable['LowerNorfair']['Main']))
|
||||
'RidleyRoomOut': Cache.ldeco(lambda sm: sm.canHellRun(**Settings.hellRunsTable['LowerNorfair']['Main'])),
|
||||
'Wasteland': Cache.ldeco(lambda sm: sm.wand(sm.canHellRun(**Settings.hellRunsTable['LowerNorfair']['Main']),
|
||||
sm.canGetBackFromRidleyZone(),
|
||||
sm.canPassWastelandDessgeegas()))
|
||||
}, internal=True),
|
||||
AccessPoint('Wasteland', 'LowerNorfair', {
|
||||
# no transition to firefleas to exlude pb of shame location when starting at firefleas top
|
||||
'Ridley Zone': Cache.ldeco(lambda sm: sm.wand(sm.canHellRun(**Settings.hellRunsTable['LowerNorfair']['Main']),
|
||||
sm.traverse('WastelandLeft'),
|
||||
sm.canGetBackFromRidleyZone(),
|
||||
sm.canPassWastelandDessgeegas(),
|
||||
sm.canPassNinjaPirates()))
|
||||
}, internal=True),
|
||||
AccessPoint('Three Muskateers Room Left', 'LowerNorfair', {
|
||||
'Firefleas': Cache.ldeco(lambda sm: sm.wand(sm.canHellRun(**Settings.hellRunsTable['LowerNorfair']['Main']),
|
||||
|
||||
@@ -797,10 +797,10 @@ locationsDict["Power Bomb (lower Norfair above fire flea room)"].Available = (
|
||||
lambda sm: SMBool(True)
|
||||
)
|
||||
locationsDict["Power Bomb (Power Bombs of shame)"].AccessFrom = {
|
||||
'Ridley Zone': lambda sm: sm.canUsePowerBombs()
|
||||
'Wasteland': lambda sm: sm.canUsePowerBombs()
|
||||
}
|
||||
locationsDict["Power Bomb (Power Bombs of shame)"].Available = (
|
||||
lambda sm: sm.canHellRun(**Settings.hellRunsTable['LowerNorfair']['Main'])
|
||||
lambda sm: SMBool(True)
|
||||
)
|
||||
locationsDict["Missile (lower Norfair near Wave Beam)"].AccessFrom = {
|
||||
'Firefleas': lambda sm: SMBool(True)
|
||||
|
||||
@@ -245,7 +245,7 @@ locBitFS_table = {
|
||||
|
||||
locWMotR_table = {
|
||||
"Wing Mario Over the Rainbow Red Coins": 3626154,
|
||||
"Wing Mario Over the Rainbow 1Up Block": 3626242
|
||||
"Wing Mario Over the Rainbow 1Up Block": 3626243
|
||||
}
|
||||
|
||||
locBitS_table = {
|
||||
@@ -268,4 +268,4 @@ location_table = {**locBoB_table,**locWhomp_table,**locJRB_table,**locCCM_table,
|
||||
**locWDW_table,**locTTM_table,**locTHI_table,**locTTC_table,**locRR_table, \
|
||||
**loc100Coin_table,**locPSS_table,**locSA_table,**locBitDW_table,**locTotWC_table, \
|
||||
**locCotMC_table, **locVCutM_table, **locBitFS_table, **locWMotR_table, **locBitS_table, \
|
||||
**locSS_table}
|
||||
**locSS_table}
|
||||
|
||||
@@ -1,48 +1,57 @@
|
||||
import typing
|
||||
from Options import Option, DefaultOnToggle, Range, Toggle, DeathLink, Choice
|
||||
|
||||
|
||||
class EnableCoinStars(DefaultOnToggle):
|
||||
"""Disable to Ignore 100 Coin Stars. You can still collect them, but they don't do anything"""
|
||||
display_name = "Enable 100 Coin Stars"
|
||||
|
||||
|
||||
class StrictCapRequirements(DefaultOnToggle):
|
||||
"""If disabled, Stars that expect special caps may have to be acquired without the caps"""
|
||||
display_name = "Strict Cap Requirements"
|
||||
|
||||
|
||||
class StrictCannonRequirements(DefaultOnToggle):
|
||||
"""If disabled, Stars that expect cannons may have to be acquired without them. Only makes a difference if Buddy Checks are enabled"""
|
||||
display_name = "Strict Cannon Requirements"
|
||||
|
||||
|
||||
class FirstBowserStarDoorCost(Range):
|
||||
"""How many stars are required at the Star Door to Bowser in the Dark World"""
|
||||
range_start = 0
|
||||
range_end = 50
|
||||
default = 8
|
||||
|
||||
|
||||
class BasementStarDoorCost(Range):
|
||||
"""How many stars are required at the Star Door in the Basement"""
|
||||
range_start = 0
|
||||
range_end = 70
|
||||
default = 30
|
||||
|
||||
|
||||
class SecondFloorStarDoorCost(Range):
|
||||
"""How many stars are required to access the third floor"""
|
||||
range_start = 0
|
||||
range_end = 90
|
||||
default = 50
|
||||
|
||||
|
||||
class MIPS1Cost(Range):
|
||||
"""How many stars are required to spawn MIPS the first time"""
|
||||
range_start = 0
|
||||
range_end = 40
|
||||
default = 15
|
||||
|
||||
|
||||
class MIPS2Cost(Range):
|
||||
"""How many stars are required to spawn MIPS the secound time. Must be bigger or equal MIPS1Cost"""
|
||||
range_start = 0
|
||||
range_end = 80
|
||||
default = 50
|
||||
|
||||
|
||||
class StarsToFinish(Range):
|
||||
"""How many stars are required at the infinite stairs"""
|
||||
display_name = "Endless Stairs Stars"
|
||||
@@ -50,35 +59,40 @@ class StarsToFinish(Range):
|
||||
range_end = 100
|
||||
default = 70
|
||||
|
||||
|
||||
class AmountOfStars(Range):
|
||||
"""How many stars exist. Disabling 100 Coin Stars removes 15 from the Pool. At least max of any Cost set"""
|
||||
range_start = 35
|
||||
range_end = 120
|
||||
default = 120
|
||||
|
||||
|
||||
class AreaRandomizer(Choice):
|
||||
"""Randomize Entrances"""
|
||||
display_name = "Entrance Randomizer"
|
||||
alias_false = 0
|
||||
option_Off = 0
|
||||
option_Courses_Only = 1
|
||||
option_Courses_and_Secrets = 2
|
||||
|
||||
|
||||
class BuddyChecks(Toggle):
|
||||
"""Bob-omb Buddies are checks, Cannon Unlocks are items"""
|
||||
display_name = "Bob-omb Buddy Checks"
|
||||
|
||||
|
||||
class ExclamationBoxes(Choice):
|
||||
"""Include 1Up Exclamation Boxes during randomization"""
|
||||
display_name = "Randomize 1Up !-Blocks"
|
||||
option_Off = 0
|
||||
option_1Ups_Only = 1
|
||||
|
||||
|
||||
class ProgressiveKeys(DefaultOnToggle):
|
||||
"""Keys will first grant you access to the Basement, then to the Secound Floor"""
|
||||
display_name = "Progressive Keys"
|
||||
|
||||
sm64_options: typing.Dict[str,type(Option)] = {
|
||||
|
||||
sm64_options: typing.Dict[str, type(Option)] = {
|
||||
"AreaRandomizer": AreaRandomizer,
|
||||
"ProgressiveKeys": ProgressiveKeys,
|
||||
"EnableCoinStars": EnableCoinStars,
|
||||
@@ -93,5 +107,5 @@ sm64_options: typing.Dict[str,type(Option)] = {
|
||||
"StarsToFinish": StarsToFinish,
|
||||
"death_link": DeathLink,
|
||||
"BuddyChecks": BuddyChecks,
|
||||
"ExclamationBoxes": ExclamationBoxes
|
||||
}
|
||||
"ExclamationBoxes": ExclamationBoxes,
|
||||
}
|
||||
|
||||
@@ -86,6 +86,7 @@ def set_rules(world, player: int, area_connections):
|
||||
# which would make it impossible to reach downtown area without the cannon.
|
||||
add_rule(world.get_location("WDW: Quick Race Through Downtown!", player), lambda state: state.has("Cannon Unlock WDW", player))
|
||||
add_rule(world.get_location("WDW: Go to Town for Red Coins", player), lambda state: state.has("Cannon Unlock WDW", player))
|
||||
add_rule(world.get_location("WDW: 1Up Block in Downtown", player), lambda state: state.has("Cannon Unlock WDW", player))
|
||||
|
||||
if world.StrictCapRequirements[player]:
|
||||
add_rule(world.get_location("BoB: Mario Wings to the Sky", player), lambda state: state.has("Wing Cap", player))
|
||||
|
||||
@@ -9,6 +9,7 @@ from .Regions import create_regions, sm64courses, sm64entrances_s, sm64_internal
|
||||
from BaseClasses import Item, Tutorial, ItemClassification
|
||||
from ..AutoWorld import World, WebWorld
|
||||
|
||||
|
||||
class SM64Web(WebWorld):
|
||||
tutorials = [Tutorial(
|
||||
"Multiworld Setup Guide",
|
||||
@@ -34,7 +35,7 @@ class SM64World(World):
|
||||
item_name_to_id = item_table
|
||||
location_name_to_id = location_table
|
||||
|
||||
data_version = 7
|
||||
data_version = 8
|
||||
required_client_version = (0, 3, 0)
|
||||
|
||||
area_connections: typing.Dict[int, int]
|
||||
|
||||
@@ -107,7 +107,6 @@ class HeartBeepSpeed(Choice):
|
||||
option_Half = 2
|
||||
option_Normal = 3
|
||||
option_Double = 4
|
||||
alias_false = 0
|
||||
default = 3
|
||||
|
||||
class HeartColor(Choice):
|
||||
|
||||
@@ -25,6 +25,10 @@ from Options import Accessibility
|
||||
world_folder = os.path.dirname(__file__)
|
||||
logger = logging.getLogger("SMZ3")
|
||||
|
||||
# Location IDs in the range 256+196 to 256+202 shifted +34 between 11.2 and 11.3
|
||||
# this is required to keep backward compatibility
|
||||
def convertLocSMZ3IDToAPID(value):
|
||||
return (value - 34) if value >= 256+230 and value <= 256+236 else value
|
||||
|
||||
class SMZ3CollectionState(metaclass=AutoLogicRegister):
|
||||
def init_mixin(self, parent: MultiWorld):
|
||||
@@ -61,12 +65,13 @@ class SMZ3World(World):
|
||||
"""
|
||||
game: str = "SMZ3"
|
||||
topology_present = False
|
||||
data_version = 2
|
||||
data_version = 3
|
||||
option_definitions = smz3_options
|
||||
item_names: Set[str] = frozenset(TotalSMZ3Item.lookup_name_to_id)
|
||||
location_names: Set[str]
|
||||
item_name_to_id = TotalSMZ3Item.lookup_name_to_id
|
||||
location_name_to_id: Dict[str, int] = {key : locations_start_id + value.Id for key, value in TotalSMZ3World(Config(), "", 0, "").locationLookup.items()}
|
||||
location_name_to_id: Dict[str, int] = {key : locations_start_id + convertLocSMZ3IDToAPID(value.Id)
|
||||
for key, value in TotalSMZ3World(Config(), "", 0, "").locationLookup.items()}
|
||||
web = SMZ3Web()
|
||||
|
||||
remote_items: bool = False
|
||||
@@ -526,9 +531,11 @@ class SMZ3World(World):
|
||||
|
||||
def JunkFillGT(self, factor):
|
||||
poolLength = len(self.world.itempool)
|
||||
playerGroups = self.world.get_player_groups(self.player)
|
||||
playerGroups.add(self.player)
|
||||
junkPoolIdx = [i for i in range(0, poolLength)
|
||||
if self.world.itempool[i].classification in (ItemClassification.filler, ItemClassification.trap) and
|
||||
self.world.itempool[i].player == self.player]
|
||||
self.world.itempool[i].player in playerGroups]
|
||||
toRemove = []
|
||||
for loc in self.locations.values():
|
||||
# commenting this for now since doing a partial GT pre fill would allow for non SMZ3 progression in GT
|
||||
|
||||
@@ -21,8 +21,6 @@ class OffOnFullChoice(Choice):
|
||||
option_on = 1
|
||||
option_full = 2
|
||||
alias_chaos = 2
|
||||
alias_false = 0
|
||||
alias_true = 1
|
||||
|
||||
|
||||
class Difficulty(EvermizerFlags, Choice):
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import functools
|
||||
from typing import Dict, Set, List
|
||||
|
||||
# EN Locale Creature Name to rough depth in meters found at
|
||||
@@ -54,21 +55,27 @@ all_creatures: Dict[str, int] = {
|
||||
"Sea Emperor Juvenile": 1700,
|
||||
}
|
||||
|
||||
# be nice and make these require Stasis Rifle
|
||||
aggressive: Set[str] = {
|
||||
"Cave Crawler", # is very easy without Stasis Rifle, but included for consistency
|
||||
"Crashfish",
|
||||
"Biter",
|
||||
"Bleeder",
|
||||
"Blighter",
|
||||
"Blood Crawler",
|
||||
"Mesmer",
|
||||
"Reaper Leviathan",
|
||||
"Crabsquid",
|
||||
"Warper",
|
||||
"Crabsnake",
|
||||
"Ampeel",
|
||||
"Stalker",
|
||||
"Sand Shark",
|
||||
"Boneshark",
|
||||
"Lava Lizard",
|
||||
"Sea Dragon Leviathan",
|
||||
"River Prowler",
|
||||
"Ghost Leviathan Juvenile",
|
||||
"Ghost Leviathan"
|
||||
}
|
||||
|
||||
containment: Set[str] = { # creatures that have to be raised from eggs
|
||||
@@ -94,6 +101,25 @@ creature_locations: Dict[str, int] = {
|
||||
creature + suffix: creature_id for creature_id, creature in enumerate(all_creatures, start=34000)
|
||||
}
|
||||
|
||||
all_creatures_presorted: List[str] = sorted(all_creatures)
|
||||
all_creatures_presorted_without_containment = [name for name in all_creatures_presorted if name not in containment]
|
||||
|
||||
class Definitions:
|
||||
"""Only compute lists if needed and then cache them."""
|
||||
|
||||
@functools.cached_property
|
||||
def all_creatures_presorted(self) -> List[str]:
|
||||
return sorted(all_creatures)
|
||||
|
||||
@functools.cached_property
|
||||
def all_creatures_presorted_without_containment(self) -> List[str]:
|
||||
return [name for name in self.all_creatures_presorted if name not in containment]
|
||||
|
||||
@functools.cached_property
|
||||
def all_creatures_presorted_without_stasis(self) -> List[str]:
|
||||
return [name for name in self.all_creatures_presorted if name not in aggressive or name in hatchable]
|
||||
|
||||
@functools.cached_property
|
||||
def all_creatures_presorted_without_aggressive(self) -> List[str]:
|
||||
return [name for name in self.all_creatures_presorted if name not in aggressive]
|
||||
|
||||
# only singleton needed
|
||||
Definitions: Definitions = Definitions()
|
||||
|
||||
@@ -139,7 +139,7 @@ item_table: Dict[int, ItemDict] = {
|
||||
'name': 'Power Transmitter Fragment',
|
||||
'tech_type': 'PowerTransmitterFragment'},
|
||||
35032: {'classification': ItemClassification.progression,
|
||||
'count': 4,
|
||||
'count': 5,
|
||||
'name': 'Prawn Suit Fragment',
|
||||
'tech_type': 'ExosuitFragment'},
|
||||
35033: {'classification': ItemClassification.useful,
|
||||
@@ -163,7 +163,7 @@ item_table: Dict[int, ItemDict] = {
|
||||
'name': 'Scanner Room Fragment',
|
||||
'tech_type': 'BaseMapRoomFragment'},
|
||||
35038: {'classification': ItemClassification.progression,
|
||||
'count': 5,
|
||||
'count': 4,
|
||||
'name': 'Seamoth Fragment',
|
||||
'tech_type': 'SeamothFragment'},
|
||||
35039: {'classification': ItemClassification.progression,
|
||||
@@ -203,9 +203,9 @@ item_table: Dict[int, ItemDict] = {
|
||||
'name': 'Picture Frame',
|
||||
'tech_type': 'PictureFrameFragment'},
|
||||
35048: {'classification': ItemClassification.filler,
|
||||
'count': 2,
|
||||
'name': 'Bench Fragment',
|
||||
'tech_type': 'BenchFragment'},
|
||||
'count': 1,
|
||||
'name': 'Bench',
|
||||
'tech_type': 'Bench'},
|
||||
35049: {'classification': ItemClassification.filler,
|
||||
'count': 1,
|
||||
'name': 'Basic Plant Pot',
|
||||
@@ -333,7 +333,12 @@ item_table: Dict[int, ItemDict] = {
|
||||
35080: {'classification': ItemClassification.filler,
|
||||
'count': 1,
|
||||
'name': 'Water Filtration Machine',
|
||||
'tech_type': 'BaseFiltrationMachine'}}
|
||||
'tech_type': 'BaseFiltrationMachine'},
|
||||
35081: {'classification': ItemClassification.progression,
|
||||
'count': 1,
|
||||
'name': 'Ultra High Capacity Tank',
|
||||
'tech_type': 'HighCapacityTank'},
|
||||
}
|
||||
|
||||
advancement_item_names: Set[str] = set()
|
||||
non_advancement_item_names: Set[str] = set()
|
||||
|
||||
@@ -15,7 +15,12 @@ class LocationDict(TypedDict, total=False):
|
||||
need_propulsion_cannon: bool
|
||||
|
||||
|
||||
events: List[str] = ["Neptune Launch", "Disable Quarantine", "Full Infection", "Repair Aurora Drive"]
|
||||
events: List[str] = [
|
||||
"Neptune Launch",
|
||||
"Disable Quarantine",
|
||||
"Full Infection",
|
||||
"Repair Aurora Drive",
|
||||
]
|
||||
|
||||
location_table: Dict[int, LocationDict] = {
|
||||
33000: {'can_slip_through': False,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import typing
|
||||
|
||||
from Options import Choice, Range, DeathLink
|
||||
from .Creatures import all_creatures
|
||||
from .Creatures import all_creatures, Definitions
|
||||
|
||||
|
||||
class ItemPool(Choice):
|
||||
@@ -46,14 +46,27 @@ class AggressiveScanLogic(Choice):
|
||||
Containment: Removes Stasis Rifle as expected solution and expects Alien Containment instead.
|
||||
Either: Creatures may be expected to be scanned via Stasis Rifle or Containment, whichever is found first.
|
||||
None: Aggressive Creatures are assumed to not need any tools to scan.
|
||||
Removed: No Creatures needing Stasis or Containment will be in the pool at all.
|
||||
|
||||
Note: Containment, Either and None adds Cuddlefish as an option for scans.
|
||||
Note: Stasis, Either and None adds unhatchable aggressive species, such as Warper.
|
||||
Note: This is purely a logic expectation, and does not affect gameplay, only placement."""
|
||||
display_name = "Aggressive Creature Scan Logic"
|
||||
option_stasis = 0
|
||||
option_containment = 1
|
||||
option_either = 2
|
||||
option_none = 3
|
||||
option_removed = 4
|
||||
|
||||
def get_pool(self) -> typing.List[str]:
|
||||
if self == self.option_removed:
|
||||
return Definitions.all_creatures_presorted_without_aggressive
|
||||
elif self == self.option_stasis:
|
||||
return Definitions.all_creatures_presorted_without_containment
|
||||
elif self == self.option_containment:
|
||||
return Definitions.all_creatures_presorted_without_stasis
|
||||
else:
|
||||
return Definitions.all_creatures_presorted
|
||||
|
||||
|
||||
class SubnauticaDeathLink(DeathLink):
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
from typing import TYPE_CHECKING, Dict, Callable
|
||||
from typing import TYPE_CHECKING, Dict, Callable, Optional
|
||||
|
||||
from worlds.generic.Rules import set_rule, add_rule
|
||||
from .Locations import location_table, LocationDict
|
||||
from .Creatures import all_creatures, aggressive, suffix
|
||||
from .Creatures import all_creatures, aggressive, suffix, hatchable, containment
|
||||
from .Options import AggressiveScanLogic
|
||||
import math
|
||||
|
||||
@@ -258,6 +258,15 @@ def set_creature_rule(world, player: int, creature_name: str) -> "Location":
|
||||
return location
|
||||
|
||||
|
||||
def get_aggression_rule(option: AggressiveScanLogic, creature_name: str) -> \
|
||||
Optional[Callable[["CollectionState", int], bool]]:
|
||||
"""Get logic rule for a creature scan location."""
|
||||
if creature_name not in hatchable and option != option.option_none: # can only be done via stasis
|
||||
return has_stasis_rifle
|
||||
# otherwise allow option preference
|
||||
return aggression_rules.get(option.value, None)
|
||||
|
||||
|
||||
aggression_rules: Dict[int, Callable[["CollectionState", int], bool]] = {
|
||||
AggressiveScanLogic.option_stasis: has_stasis_rifle,
|
||||
AggressiveScanLogic.option_containment: has_containment,
|
||||
@@ -274,14 +283,21 @@ def set_rules(subnautica_world: "SubnauticaWorld"):
|
||||
set_location_rule(world, player, loc)
|
||||
|
||||
if subnautica_world.creatures_to_scan:
|
||||
aggressive_rule = aggression_rules.get(world.creature_scan_logic[player], None)
|
||||
option = world.creature_scan_logic[player]
|
||||
|
||||
for creature_name in subnautica_world.creatures_to_scan:
|
||||
location = set_creature_rule(world, player, creature_name)
|
||||
if creature_name in aggressive and aggressive_rule:
|
||||
add_rule(location, lambda state: aggressive_rule(state, player))
|
||||
if creature_name in containment: # there is no other way, hard-required containment
|
||||
add_rule(location, lambda state: has_containment(state, player))
|
||||
elif creature_name in aggressive:
|
||||
rule = get_aggression_rule(option, creature_name)
|
||||
if rule:
|
||||
add_rule(location,
|
||||
lambda state, loc_rule=get_aggression_rule(option, creature_name): loc_rule(state, player))
|
||||
|
||||
# Victory locations
|
||||
set_rule(world.get_location("Neptune Launch", player), lambda state:
|
||||
set_rule(world.get_location("Neptune Launch", player),
|
||||
lambda state:
|
||||
get_max_depth(state, player) >= 1444 and
|
||||
has_mobile_vehicle_bay(state, player) and
|
||||
state.has("Neptune Launch Platform", player) and
|
||||
|
||||
@@ -41,7 +41,7 @@ class SubnauticaWorld(World):
|
||||
location_name_to_id = all_locations
|
||||
option_definitions = Options.options
|
||||
|
||||
data_version = 6
|
||||
data_version = 7
|
||||
required_client_version = (0, 3, 5)
|
||||
|
||||
prefill_items: List[Item]
|
||||
@@ -52,14 +52,15 @@ class SubnauticaWorld(World):
|
||||
self.create_item("Seaglide Fragment"),
|
||||
self.create_item("Seaglide Fragment")
|
||||
]
|
||||
if self.world.creature_scan_logic[self.player] == Options.AggressiveScanLogic.option_stasis:
|
||||
valid_creatures = Creatures.all_creatures_presorted_without_containment
|
||||
self.world.creature_scans[self.player].value = min(len(
|
||||
Creatures.all_creatures_presorted_without_containment),
|
||||
self.world.creature_scans[self.player].value)
|
||||
else:
|
||||
valid_creatures = Creatures.all_creatures_presorted
|
||||
self.creatures_to_scan = self.world.random.sample(valid_creatures,
|
||||
scan_option: Options.AggressiveScanLogic = self.world.creature_scan_logic[self.player]
|
||||
creature_pool = scan_option.get_pool()
|
||||
|
||||
self.world.creature_scans[self.player].value = min(
|
||||
len(creature_pool),
|
||||
self.world.creature_scans[self.player].value
|
||||
)
|
||||
|
||||
self.creatures_to_scan = self.world.random.sample(creature_pool,
|
||||
self.world.creature_scans[self.player].value)
|
||||
|
||||
def create_regions(self):
|
||||
|
||||
@@ -3,66 +3,82 @@ from BaseClasses import MultiWorld
|
||||
from Options import Toggle, DefaultOnToggle, DeathLink, Choice, Range, Option, OptionDict
|
||||
from schema import Schema, And, Optional
|
||||
|
||||
|
||||
class StartWithJewelryBox(Toggle):
|
||||
"Start with Jewelry Box unlocked"
|
||||
display_name = "Start with Jewelry Box"
|
||||
|
||||
|
||||
#class ProgressiveVerticalMovement(Toggle):
|
||||
# "Always find vertical movement in the following order Succubus Hairpin -> Light Wall -> Celestial Sash"
|
||||
# display_name = "Progressive vertical movement"
|
||||
|
||||
|
||||
#class ProgressiveKeycards(Toggle):
|
||||
# "Always find Security Keycard's in the following order D -> C -> B -> A"
|
||||
# display_name = "Progressive keycards"
|
||||
|
||||
|
||||
class DownloadableItems(DefaultOnToggle):
|
||||
"With the tablet you will be able to download items at terminals"
|
||||
display_name = "Downloadable items"
|
||||
|
||||
|
||||
class EyeSpy(Toggle):
|
||||
"Requires Oculus Ring in inventory to be able to break hidden walls."
|
||||
display_name = "Eye Spy"
|
||||
|
||||
|
||||
class StartWithMeyef(Toggle):
|
||||
"Start with Meyef, ideal for when you want to play multiplayer."
|
||||
display_name = "Start with Meyef"
|
||||
|
||||
|
||||
class QuickSeed(Toggle):
|
||||
"Start with Talaria Attachment, Nyoom!"
|
||||
display_name = "Quick seed"
|
||||
|
||||
|
||||
class SpecificKeycards(Toggle):
|
||||
"Keycards can only open corresponding doors"
|
||||
display_name = "Specific Keycards"
|
||||
|
||||
|
||||
class Inverted(Toggle):
|
||||
"Start in the past"
|
||||
display_name = "Inverted"
|
||||
|
||||
|
||||
#class StinkyMaw(Toggle):
|
||||
# "Require gasmask for Maw"
|
||||
# display_name = "Stinky Maw"
|
||||
|
||||
|
||||
class GyreArchives(Toggle):
|
||||
"Gyre locations are in logic. New warps are gated by Merchant Crow and Kobo"
|
||||
display_name = "Gyre Archives"
|
||||
|
||||
|
||||
class Cantoran(Toggle):
|
||||
"Cantoran's fight and check are available upon revisiting his room"
|
||||
display_name = "Cantoran"
|
||||
|
||||
|
||||
class LoreChecks(Toggle):
|
||||
"Memories and journal entries contain items."
|
||||
display_name = "Lore Checks"
|
||||
|
||||
|
||||
class BossRando(Toggle):
|
||||
"Shuffles the positions of all bosses."
|
||||
display_name = "Boss Randomization"
|
||||
|
||||
|
||||
class BossScaling(DefaultOnToggle):
|
||||
"When Boss Rando is enabled, scales the bosses' HP, XP, and ATK to the stats of the location they replace (Reccomended)"
|
||||
display_name = "Scale Random Boss Stats"
|
||||
|
||||
|
||||
class DamageRando(Choice):
|
||||
"Randomly nerfs and buffs some orbs and their associated spells as well as some associated rings."
|
||||
display_name = "Damage Rando"
|
||||
@@ -73,9 +89,9 @@ class DamageRando(Choice):
|
||||
option_mostlybuffs = 4
|
||||
option_allbuffs = 5
|
||||
option_manual = 6
|
||||
alias_false = 0
|
||||
alias_true = 2
|
||||
|
||||
|
||||
class DamageRandoOverrides(OptionDict):
|
||||
"Manual +/-/normal odds for an orb. Put 0 if you don't want a certain nerf or buff to be a possibility. Orbs that you don't specify will roll with 1/1/1 as odds"
|
||||
schema = Schema({
|
||||
@@ -180,6 +196,7 @@ class DamageRandoOverrides(OptionDict):
|
||||
"Radiant": { "MinusOdds": 1, "NormalOdds": 1, "PlusOdds": 1 },
|
||||
}
|
||||
|
||||
|
||||
class HpCap(Range):
|
||||
"Sets the number that Lunais's HP maxes out at."
|
||||
display_name = "HP Cap"
|
||||
@@ -187,10 +204,12 @@ class HpCap(Range):
|
||||
range_end = 999
|
||||
default = 999
|
||||
|
||||
|
||||
class BossHealing(DefaultOnToggle):
|
||||
"Enables/disables healing after boss fights. NOTE: Currently only applicable when Boss Rando is enabled."
|
||||
display_name = "Heal After Bosses"
|
||||
|
||||
|
||||
class ShopFill(Choice):
|
||||
"""Sets the items for sale in Merchant Crow's shops.
|
||||
Default: No sunglasses or trendy jacket, but sand vials for sale.
|
||||
@@ -203,10 +222,12 @@ class ShopFill(Choice):
|
||||
option_vanilla = 2
|
||||
option_empty = 3
|
||||
|
||||
|
||||
class ShopWarpShards(DefaultOnToggle):
|
||||
"Shops always sell warp shards (when keys possessed), ignoring inventory setting."
|
||||
display_name = "Always Sell Warp Shards"
|
||||
|
||||
|
||||
class ShopMultiplier(Range):
|
||||
"Multiplier for the cost of items in the shop. Set to 0 for free shops."
|
||||
display_name = "Shop Price Multiplier"
|
||||
@@ -214,6 +235,7 @@ class ShopMultiplier(Range):
|
||||
range_end = 10
|
||||
default = 1
|
||||
|
||||
|
||||
class LootPool(Choice):
|
||||
"""Sets the items that drop from enemies (does not apply to boss reward checks)
|
||||
Vanilla: Drops are the same as the base game
|
||||
@@ -224,6 +246,7 @@ class LootPool(Choice):
|
||||
option_randomized = 1
|
||||
option_empty = 2
|
||||
|
||||
|
||||
class DropRateCategory(Choice):
|
||||
"""Sets the drop rate when 'Loot Pool' is set to 'Random'
|
||||
Tiered: Based on item rarity/value
|
||||
@@ -237,6 +260,7 @@ class DropRateCategory(Choice):
|
||||
option_randomized = 2
|
||||
option_fixed = 3
|
||||
|
||||
|
||||
class FixedDropRate(Range):
|
||||
"Base drop rate percentage when 'Drop Rate Category' is set to 'Fixed'"
|
||||
display_name = "Fixed Drop Rate"
|
||||
@@ -244,6 +268,7 @@ class FixedDropRate(Range):
|
||||
range_end = 100
|
||||
default = 5
|
||||
|
||||
|
||||
class LootTierDistro(Choice):
|
||||
"""Sets how often items of each rarity tier are placed when 'Loot Pool' is set to 'Random'
|
||||
Default Weight: Rarer items will be assigned to enemy drop slots less frequently than common items
|
||||
@@ -255,14 +280,17 @@ class LootTierDistro(Choice):
|
||||
option_full_random = 1
|
||||
option_inverted_weight = 2
|
||||
|
||||
|
||||
class ShowBestiary(Toggle):
|
||||
"All entries in the bestiary are visible, without needing to kill one of a given enemy first"
|
||||
display_name = "Show Bestiary Entries"
|
||||
|
||||
|
||||
class ShowDrops(Toggle):
|
||||
"All item drops in the bestiary are visible, without needing an enemy to drop one of a given item first"
|
||||
display_name = "Show Bestiary Item Drops"
|
||||
|
||||
|
||||
# Some options that are available in the timespinner randomizer arent currently implemented
|
||||
timespinner_options: Dict[str, Option] = {
|
||||
"StartWithJewelryBox": StartWithJewelryBox,
|
||||
@@ -296,9 +324,11 @@ timespinner_options: Dict[str, Option] = {
|
||||
"DeathLink": DeathLink,
|
||||
}
|
||||
|
||||
|
||||
def is_option_enabled(world: MultiWorld, player: int, name: str) -> bool:
|
||||
return get_option_value(world, player, name) > 0
|
||||
|
||||
|
||||
def get_option_value(world: MultiWorld, player: int, name: str) -> Union[int, dict]:
|
||||
option = getattr(world, name, None)
|
||||
if option == None:
|
||||
|
||||
Reference in New Issue
Block a user