mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-03-22 15:45:04 -07:00
Merge branch 'main' into setup_flip_apworld_list
This commit is contained in:
@@ -23,6 +23,7 @@ from NetUtils import Endpoint, decode, NetworkItem, encode, JSONtoTextParser, \
|
||||
from Utils import Version, stream_input, async_start
|
||||
from worlds import network_data_package, AutoWorldRegister
|
||||
import os
|
||||
import ssl
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
import kvui
|
||||
@@ -33,6 +34,12 @@ logger = logging.getLogger("Client")
|
||||
gui_enabled = not sys.stdout or "--nogui" not in sys.argv
|
||||
|
||||
|
||||
@Utils.cache_argsless
|
||||
def get_ssl_context():
|
||||
import certifi
|
||||
return ssl.create_default_context(ssl.Purpose.SERVER_AUTH, cafile=certifi.where())
|
||||
|
||||
|
||||
class ClientCommandProcessor(CommandProcessor):
|
||||
def __init__(self, ctx: CommonContext):
|
||||
self.ctx = ctx
|
||||
@@ -589,7 +596,8 @@ async def server_loop(ctx: CommonContext, address: typing.Optional[str] = None)
|
||||
|
||||
logger.info(f'Connecting to Archipelago server at {address}')
|
||||
try:
|
||||
socket = await websockets.connect(address, port=port, ping_timeout=None, ping_interval=None)
|
||||
socket = await websockets.connect(address, port=port, ping_timeout=None, ping_interval=None,
|
||||
ssl=get_ssl_context() if address.startswith("wss://") else None)
|
||||
if ctx.ui is not None:
|
||||
ctx.ui.update_address_bar(server_url.netloc)
|
||||
ctx.server = Endpoint(socket)
|
||||
@@ -604,6 +612,7 @@ async def server_loop(ctx: CommonContext, address: typing.Optional[str] = None)
|
||||
except websockets.InvalidMessage:
|
||||
# probably encrypted
|
||||
if address.startswith("ws://"):
|
||||
# try wss
|
||||
await server_loop(ctx, "ws" + address[1:])
|
||||
else:
|
||||
ctx.handle_connection_loss(f"Lost connection to the multiworld server due to InvalidMessage"
|
||||
|
||||
52
Generate.py
52
Generate.py
@@ -447,6 +447,11 @@ def roll_settings(weights: dict, plando_options: PlandoOptions = PlandoOptions.b
|
||||
raise Exception(f"Option {option_key} has to be in a game's section, not on its own.")
|
||||
|
||||
ret.game = get_choice("game", weights)
|
||||
if ret.game not in AutoWorldRegister.world_types:
|
||||
picks = Utils.get_fuzzy_results(ret.game, AutoWorldRegister.world_types, limit=1)[0]
|
||||
raise Exception(f"No world found to handle game {ret.game}. Did you mean '{picks[0]}' ({picks[1]}% sure)? "
|
||||
f"Check your spelling or installation of that world.")
|
||||
|
||||
if ret.game not in weights:
|
||||
raise Exception(f"No game options for selected game \"{ret.game}\" found.")
|
||||
|
||||
@@ -461,32 +466,29 @@ def roll_settings(weights: dict, plando_options: PlandoOptions = PlandoOptions.b
|
||||
for option_key, option in Options.common_options.items():
|
||||
setattr(ret, option_key, option.from_any(get_choice(option_key, weights, option.default)))
|
||||
|
||||
if ret.game in AutoWorldRegister.world_types:
|
||||
for option_key, option in world_type.option_definitions.items():
|
||||
for option_key, option in world_type.option_definitions.items():
|
||||
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 option_key not in world_type.option_definitions and \
|
||||
(option_key not in Options.common_options or option_key in game_weights):
|
||||
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 option_key not in world_type.option_definitions and \
|
||||
(option_key not in Options.common_options or option_key in game_weights):
|
||||
handle_option(ret, game_weights, option_key, option, plando_options)
|
||||
if PlandoOptions.items in plando_options:
|
||||
ret.plando_items = game_weights.get("plando_items", [])
|
||||
if ret.game == "Minecraft" or ret.game == "Ocarina of Time":
|
||||
# bad hardcoded behavior to make this work for now
|
||||
ret.plando_connections = []
|
||||
if PlandoOptions.connections in plando_options:
|
||||
options = game_weights.get("plando_connections", [])
|
||||
for placement in options:
|
||||
if roll_percentage(get_choice("percentage", placement, 100)):
|
||||
ret.plando_connections.append(PlandoConnection(
|
||||
get_choice("entrance", placement),
|
||||
get_choice("exit", placement),
|
||||
get_choice("direction", placement)
|
||||
))
|
||||
elif ret.game == "A Link to the Past":
|
||||
roll_alttp_settings(ret, game_weights, plando_options)
|
||||
else:
|
||||
raise Exception(f"Unsupported game {ret.game}")
|
||||
if PlandoOptions.items in plando_options:
|
||||
ret.plando_items = game_weights.get("plando_items", [])
|
||||
if ret.game == "Minecraft" or ret.game == "Ocarina of Time":
|
||||
# bad hardcoded behavior to make this work for now
|
||||
ret.plando_connections = []
|
||||
if PlandoOptions.connections in plando_options:
|
||||
options = game_weights.get("plando_connections", [])
|
||||
for placement in options:
|
||||
if roll_percentage(get_choice("percentage", placement, 100)):
|
||||
ret.plando_connections.append(PlandoConnection(
|
||||
get_choice("entrance", placement),
|
||||
get_choice("exit", placement),
|
||||
get_choice("direction", placement)
|
||||
))
|
||||
elif ret.game == "A Link to the Past":
|
||||
roll_alttp_settings(ret, game_weights, plando_options)
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
@@ -92,7 +92,7 @@ def roll_options(options: Dict[str, Union[dict, str]],
|
||||
rolled_results[f"{filename}/{i + 1}"] = roll_settings(yaml_data,
|
||||
plando_options=plando_options)
|
||||
except Exception as e:
|
||||
results[filename] = f"Failed to generate mystery in {filename}: {e}"
|
||||
results[filename] = f"Failed to generate options in {filename}: {e}"
|
||||
else:
|
||||
results[filename] = True
|
||||
return results, rolled_results
|
||||
|
||||
@@ -169,13 +169,11 @@ def run_server_process(room_id, ponyconfig: dict, static_server_data: dict,
|
||||
ctx.init_save()
|
||||
ssl_context = load_server_cert(cert_file, cert_key_file) if cert_file else None
|
||||
try:
|
||||
ctx.server = websockets.serve(functools.partial(server, ctx=ctx), ctx.host, ctx.port, ping_timeout=None,
|
||||
ping_interval=None, ssl=ssl_context)
|
||||
ctx.server = websockets.serve(functools.partial(server, ctx=ctx), ctx.host, ctx.port, ssl=ssl_context)
|
||||
|
||||
await ctx.server
|
||||
except Exception: # likely port in use - in windows this is OSError, but I didn't check the others
|
||||
ctx.server = websockets.serve(functools.partial(server, ctx=ctx), ctx.host, 0, ping_timeout=None,
|
||||
ping_interval=None, ssl=ssl_context)
|
||||
ctx.server = websockets.serve(functools.partial(server, ctx=ctx), ctx.host, 0, ssl=ssl_context)
|
||||
|
||||
await ctx.server
|
||||
port = 0
|
||||
|
||||
@@ -148,7 +148,7 @@ const buildOptionsTable = (settings, romOpts = false) => {
|
||||
randomButton.classList.add('randomize-button');
|
||||
randomButton.setAttribute('data-key', setting);
|
||||
randomButton.setAttribute('data-tooltip', 'Toggle randomization for this option!');
|
||||
randomButton.addEventListener('click', (event) => toggleRandomize(event, [select]));
|
||||
randomButton.addEventListener('click', (event) => toggleRandomize(event, select));
|
||||
if (currentSettings[gameName][setting] === 'random') {
|
||||
randomButton.classList.add('active');
|
||||
select.disabled = true;
|
||||
@@ -185,7 +185,7 @@ const buildOptionsTable = (settings, romOpts = false) => {
|
||||
randomButton.classList.add('randomize-button');
|
||||
randomButton.setAttribute('data-key', setting);
|
||||
randomButton.setAttribute('data-tooltip', 'Toggle randomization for this option!');
|
||||
randomButton.addEventListener('click', (event) => toggleRandomize(event, [range]));
|
||||
randomButton.addEventListener('click', (event) => toggleRandomize(event, range));
|
||||
if (currentSettings[gameName][setting] === 'random') {
|
||||
randomButton.classList.add('active');
|
||||
range.disabled = true;
|
||||
@@ -269,7 +269,7 @@ const buildOptionsTable = (settings, romOpts = false) => {
|
||||
randomButton.setAttribute('data-key', setting);
|
||||
randomButton.setAttribute('data-tooltip', 'Toggle randomization for this option!');
|
||||
randomButton.addEventListener('click', (event) => toggleRandomize(
|
||||
event, [specialRange, specialRangeSelect])
|
||||
event, specialRange, specialRangeSelect)
|
||||
);
|
||||
if (currentSettings[gameName][setting] === 'random') {
|
||||
randomButton.classList.add('active');
|
||||
@@ -294,23 +294,25 @@ const buildOptionsTable = (settings, romOpts = false) => {
|
||||
return table;
|
||||
};
|
||||
|
||||
const toggleRandomize = (event, inputElements) => {
|
||||
const toggleRandomize = (event, inputElement, optionalSelectElement = null) => {
|
||||
const active = event.target.classList.contains('active');
|
||||
const randomButton = event.target;
|
||||
|
||||
if (active) {
|
||||
randomButton.classList.remove('active');
|
||||
for (const element of inputElements) {
|
||||
element.disabled = undefined;
|
||||
updateGameSetting(element);
|
||||
inputElement.disabled = undefined;
|
||||
if (optionalSelectElement) {
|
||||
optionalSelectElement.disabled = undefined;
|
||||
}
|
||||
} else {
|
||||
randomButton.classList.add('active');
|
||||
for (const element of inputElements) {
|
||||
element.disabled = true;
|
||||
updateGameSetting(randomButton);
|
||||
inputElement.disabled = true;
|
||||
if (optionalSelectElement) {
|
||||
optionalSelectElement.disabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
updateGameSetting(randomButton);
|
||||
};
|
||||
|
||||
const updateBaseSetting = (event) => {
|
||||
@@ -364,6 +366,7 @@ const generateGame = (raceMode = false) => {
|
||||
weights: { player: settings },
|
||||
presetData: { player: settings },
|
||||
playerCount: 1,
|
||||
spoiler: 3,
|
||||
race: raceMode ? '1' : '0',
|
||||
}).then((response) => {
|
||||
window.location.href = response.data.url;
|
||||
|
||||
@@ -1199,6 +1199,7 @@ const generateGame = (raceMode = false) => {
|
||||
weights: { player: JSON.stringify(settings) },
|
||||
presetData: { player: JSON.stringify(settings) },
|
||||
playerCount: 1,
|
||||
spoiler: 3,
|
||||
race: raceMode ? '1' : '0',
|
||||
}).then((response) => {
|
||||
window.location.href = response.data.url;
|
||||
|
||||
@@ -341,3 +341,4 @@ The various methods and attributes are documented in `/worlds/AutoWorld.py[World
|
||||
[world api.md](https://github.com/ArchipelagoMW/Archipelago/blob/main/docs/world%20api.md),
|
||||
though it is also recommended to look at existing implementations to see how all this works first-hand.
|
||||
Once you get all that, all that remains to do is test the game and publish your work.
|
||||
Make sure to check out [world maintainer.md](./world%20maintainer.md) before publishing.
|
||||
|
||||
@@ -10,3 +10,5 @@ Otherwise, we tend to judge code on a case to case basis.
|
||||
For adding a new game to Archipelago and other documentation on how Archipelago functions, please see
|
||||
[the docs folder](/docs/) for the relevant information and feel free to ask any questions in the #archipelago-dev
|
||||
channel in our [Discord](https://archipelago.gg/discord).
|
||||
If you want to merge a new game, please make sure to read the responsibilities as
|
||||
[world maintainer](/docs/world%20maintainer.md).
|
||||
|
||||
@@ -69,6 +69,19 @@ It should be dropped as "SNI" into the root folder of the project. Alternatively
|
||||
host.yaml at your SNI folder.
|
||||
|
||||
|
||||
## Optional: Git
|
||||
|
||||
[Git](https://git-scm.com) is required to install some of the packages that Archipelago depends on.
|
||||
It may be possible to run Archipelago from source without it, at your own risk.
|
||||
|
||||
It is also generally recommended to have Git installed and understand how to use it, especially if you're thinking about contributing.
|
||||
|
||||
You can download the latest release of Git at [The downloads page on the Git website](https://git-scm.com/downloads).
|
||||
|
||||
Beyond that, there are also graphical interfaces for Git that make it more accessible.
|
||||
For repositories on Github (such as this one), [Github Desktop](https://desktop.github.com) is one such option.
|
||||
PyCharm has a built-in version control integration that supports Git.
|
||||
|
||||
## Running tests
|
||||
|
||||
Run `pip install pytest pytest-subtests`, then use your IDE to run tests or run `pytest` from the source folder.
|
||||
|
||||
60
docs/world maintainer.md
Normal file
60
docs/world maintainer.md
Normal file
@@ -0,0 +1,60 @@
|
||||
# World Maintainer
|
||||
|
||||
A world maintainer is a person responsible for a world or part of a world in Archipelago.
|
||||
|
||||
If a world author does not want to take on the responsibilities of a world maintainer, they can release their world as
|
||||
an unofficial [APWorld](/docs/apworld%20specification.md) or maintain their own fork instead.
|
||||
|
||||
|
||||
## Responsibilities
|
||||
|
||||
Unless these are shared between multiple people, we expect the following from each world maintainer
|
||||
|
||||
* Be on our Discord to get updates on problems with and suggestions for the world.
|
||||
* Decide if a feature (pull request) should be merged.
|
||||
* Review contents of such pull requests or organize peer reviews or post that you did not review the content.
|
||||
* Fix or point out issues when core changes break your code.
|
||||
* Use the watch function on GitHub, the #github-updates channel on Discord or check manually from time to time for new
|
||||
pull requests. Core maintainers may also ping you if a pull request concerns your world.
|
||||
* Test (or have tested) the world on the main branch from time to time, especially during RC (release candidate) phases
|
||||
of development.
|
||||
* Let us know of long unavailabilities.
|
||||
|
||||
|
||||
## Becoming a World Maintainer
|
||||
|
||||
### Adding a World
|
||||
|
||||
When we merge your world into the core Archipelago repository, you automatically become world maintainer unless you
|
||||
nominate someone else (i.e. there are multiple devs).
|
||||
|
||||
### Getting Voted
|
||||
|
||||
When a world is unmaintained, the [core maintainers](https://github.com/orgs/ArchipelagoMW/people)
|
||||
can vote for a new maintainer if there is a candidate.
|
||||
For a vote to pass, the majority of participating core maintainers must vote in the affirmative.
|
||||
The time limit is 1 week, but can end early if the majority is reached earlier.
|
||||
Voting shall be conducted on Discord in #archipelago-dev.
|
||||
|
||||
|
||||
## Dropping out
|
||||
|
||||
### Resigning
|
||||
|
||||
A world maintainer can resign. If no new maintainer steps up and gets voted, the world becomes unmaintained.
|
||||
|
||||
### Getting Voted out
|
||||
|
||||
A world maintainer can be voted out by the [core maintainers](https://github.com/orgs/ArchipelagoMW/people),
|
||||
for example when they become unreachable.
|
||||
For a vote to pass, the majority of participating core maintainers must vote in the affirmative.
|
||||
The time limit is 2 weeks, but can end early if the majority is reached earlier AND the world maintainer was pinged and
|
||||
made their case or was pinged and has been unreachable for more than 2 weeks already.
|
||||
Voting shall be conducted on Discord in #archipelago-dev. Commits that are a direct result of the voting shall include
|
||||
date, voting members and final result in the commit message.
|
||||
|
||||
|
||||
## Handling of Unmaintained Worlds
|
||||
|
||||
As long as worlds are known to work for the most part, they can stay included. Once a world becomes broken it shall be
|
||||
moved from `worlds/` to `worlds_disabled/`.
|
||||
@@ -7,3 +7,4 @@ schema>=0.7.5
|
||||
kivy>=2.2.0
|
||||
bsdiff4>=1.2.3
|
||||
platformdirs>=3.5.1
|
||||
certifi>=2023.5.7
|
||||
|
||||
@@ -32,12 +32,16 @@ class DungeonItemData(ItemData):
|
||||
s = self.ladxr_id[:-1]
|
||||
return DungeonItemType.__dict__[s]
|
||||
|
||||
|
||||
class TradeItemData(ItemData):
|
||||
vanilla_location = None
|
||||
|
||||
def __new__(cls, item_name, ladxr_id, classification, vanilla_location):
|
||||
self = super(ItemData, cls).__new__(cls, (item_name, ladxr_id, classification))
|
||||
self.vanilla_location = vanilla_location
|
||||
return self
|
||||
|
||||
|
||||
class LinksAwakeningItem(Item):
|
||||
game: str = Common.LINKS_AWAKENING
|
||||
|
||||
@@ -49,6 +53,7 @@ class LinksAwakeningItem(Item):
|
||||
super().__init__(item_data.item_name, classification, Common.BASE_ID + item_data.item_id, player)
|
||||
self.item_data = item_data
|
||||
|
||||
|
||||
# TODO: use _NAMES instead?
|
||||
class ItemName:
|
||||
POWER_BRACELET = "Progressive Power Bracelet"
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
from BaseClasses import Region, Entrance, Location
|
||||
from worlds.AutoWorld import LogicMixin
|
||||
from BaseClasses import Region, Entrance, Location, CollectionState
|
||||
|
||||
|
||||
from .LADXR.checkMetadata import checkMetadataTable
|
||||
from .Common import *
|
||||
from worlds.generic.Rules import add_item_rule
|
||||
from .Items import ladxr_item_to_la_item_name, ItemName, LinksAwakeningItem
|
||||
from .LADXR.locations.tradeSequence import TradeRequirements, TradeSequenceItem
|
||||
from .Items import ladxr_item_to_la_item_name
|
||||
|
||||
|
||||
prefilled_events = ["ANGLER_KEYHOLE", "RAFT", "MEDICINE2", "CASTLE_BUTTON"]
|
||||
|
||||
@@ -80,27 +79,19 @@ class LinksAwakeningLocation(Location):
|
||||
add_item_rule(self, filter_item)
|
||||
|
||||
|
||||
def has_free_weapon(state: "CollectionState", player: int) -> bool:
|
||||
def has_free_weapon(state: CollectionState, player: int) -> bool:
|
||||
return state.has("Progressive Sword", player) or state.has("Magic Rod", player) or state.has("Boomerang", player) or state.has("Hookshot", player)
|
||||
|
||||
|
||||
# If the player has access to farm enough rupees to afford a game, we assume that they can keep beating the game
|
||||
def can_farm_rupees(state: "CollectionState", player: int) -> bool:
|
||||
def can_farm_rupees(state: CollectionState, player: int) -> bool:
|
||||
return has_free_weapon(state, player) and (state.has("Can Play Trendy Game", player=player) or state.has("RAFT", player=player))
|
||||
|
||||
|
||||
class LinksAwakeningLogic(LogicMixin):
|
||||
rupees = {
|
||||
ItemName.RUPEES_20: 0,
|
||||
ItemName.RUPEES_50: 0,
|
||||
ItemName.RUPEES_100: 100,
|
||||
ItemName.RUPEES_200: 200,
|
||||
ItemName.RUPEES_500: 500,
|
||||
}
|
||||
|
||||
def get_credits(self, player: int):
|
||||
if can_farm_rupees(self, player):
|
||||
return 999999999
|
||||
return sum(self.count(item_name, player) * amount for item_name, amount in self.rupees.items())
|
||||
def get_credits(state: CollectionState, player: int):
|
||||
if can_farm_rupees(state, player):
|
||||
return 999999999
|
||||
return state.prog_items["RUPEES", player]
|
||||
|
||||
|
||||
class LinksAwakeningRegion(Region):
|
||||
@@ -137,7 +128,7 @@ class GameStateAdapater:
|
||||
|
||||
def get(self, item, default):
|
||||
if item == "RUPEES":
|
||||
return self.state.get_credits(self.player)
|
||||
return get_credits(self.state, self.player)
|
||||
elif item.endswith("_USED"):
|
||||
return 0
|
||||
else:
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import binascii
|
||||
import bsdiff4
|
||||
import itertools
|
||||
import os
|
||||
import pkgutil
|
||||
import tempfile
|
||||
@@ -12,7 +11,7 @@ from worlds.AutoWorld import WebWorld, World
|
||||
from .Common import *
|
||||
from .Items import (DungeonItemData, DungeonItemType, LinksAwakeningItem, TradeItemData,
|
||||
ladxr_item_to_la_item_name, links_awakening_items,
|
||||
links_awakening_items_by_name)
|
||||
links_awakening_items_by_name, ItemName)
|
||||
from .LADXR import generator
|
||||
from .LADXR.itempool import ItemPool as LADXRItemPool
|
||||
from .LADXR.logic import Logic as LAXDRLogic
|
||||
@@ -29,6 +28,7 @@ from .Rom import LADXDeltaPatch
|
||||
|
||||
DEVELOPER_MODE = False
|
||||
|
||||
|
||||
class LinksAwakeningWebWorld(WebWorld):
|
||||
tutorials = [Tutorial(
|
||||
"Multiworld Setup Guide",
|
||||
@@ -45,7 +45,7 @@ class LinksAwakeningWorld(World):
|
||||
After a previous adventure, Link is stranded on Koholint Island, full of mystery and familiar faces.
|
||||
Gather the 8 Instruments of the Sirens to wake the Wind Fish, so that Link can go home!
|
||||
"""
|
||||
game: str = LINKS_AWAKENING # name of the game/world
|
||||
game = LINKS_AWAKENING # name of the game/world
|
||||
web = LinksAwakeningWebWorld()
|
||||
|
||||
option_definitions = links_awakening_options # options the player can set
|
||||
@@ -82,6 +82,14 @@ class LinksAwakeningWorld(World):
|
||||
|
||||
player_options = None
|
||||
|
||||
rupees = {
|
||||
ItemName.RUPEES_20: 0,
|
||||
ItemName.RUPEES_50: 0,
|
||||
ItemName.RUPEES_100: 100,
|
||||
ItemName.RUPEES_200: 200,
|
||||
ItemName.RUPEES_500: 500,
|
||||
}
|
||||
|
||||
def convert_ap_options_to_ladxr_logic(self):
|
||||
self.player_options = {
|
||||
option: getattr(self.multiworld, option)[self.player] for option in self.option_definitions
|
||||
@@ -95,7 +103,6 @@ class LinksAwakeningWorld(World):
|
||||
self.ladxr_logic = LAXDRLogic(configuration_options=self.laxdr_options, world_setup=world_setup)
|
||||
self.ladxr_itempool = LADXRItemPool(self.ladxr_logic, self.laxdr_options, self.multiworld.random).toDict()
|
||||
|
||||
|
||||
def create_regions(self) -> None:
|
||||
# Initialize
|
||||
self.convert_ap_options_to_ladxr_logic()
|
||||
@@ -401,9 +408,6 @@ class LinksAwakeningWorld(World):
|
||||
|
||||
return "TRADING_ITEM_LETTER"
|
||||
|
||||
|
||||
|
||||
|
||||
def generate_output(self, output_directory: str):
|
||||
# copy items back to locations
|
||||
for r in self.multiworld.get_regions(self.player):
|
||||
@@ -464,9 +468,8 @@ class LinksAwakeningWorld(World):
|
||||
bsdiff4.file_patch_inplace(out_path, title_patch.name)
|
||||
os.unlink(title_patch.name)
|
||||
|
||||
|
||||
patch = LADXDeltaPatch(os.path.splitext(out_path)[0]+LADXDeltaPatch.patch_file_ending, player=self.player,
|
||||
player_name=self.multiworld.player_name[self.player], patched_path=out_path)
|
||||
player_name=self.multiworld.player_name[self.player], patched_path=out_path)
|
||||
patch.write()
|
||||
if not DEVELOPER_MODE:
|
||||
os.unlink(out_path)
|
||||
@@ -475,4 +478,20 @@ class LinksAwakeningWorld(World):
|
||||
return bytearray(self.multiworld.random.getrandbits(8) for _ in range(10)) + self.player.to_bytes(2, 'big')
|
||||
|
||||
def modify_multidata(self, multidata: dict):
|
||||
multidata["connect_names"][binascii.hexlify(self.multi_key).decode()] = multidata["connect_names"][self.multiworld.player_name[self.player]]
|
||||
multidata["connect_names"][binascii.hexlify(self.multi_key).decode()] = multidata["connect_names"][self.multiworld.player_name[self.player]]
|
||||
|
||||
def collect(self, state, item: Item) -> bool:
|
||||
change = super().collect(state, item)
|
||||
if change:
|
||||
rupees = self.rupees.get(item.name, 0)
|
||||
state.prog_items["RUPEES", item.player] += rupees
|
||||
|
||||
return change
|
||||
|
||||
def remove(self, state, item: Item) -> bool:
|
||||
change = super().remove(state, item)
|
||||
if change:
|
||||
rupees = self.rupees.get(item.name, 0)
|
||||
state.prog_items["RUPEES", item.player] -= rupees
|
||||
|
||||
return change
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from ..generic.Rules import add_rule
|
||||
from worlds.generic.Rules import add_rule
|
||||
from .Locations import food_locations, shop_locations
|
||||
from .ItemPool import dangerous_weapon_locations
|
||||
from .Options import StartingPosition
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import logging
|
||||
import os
|
||||
import threading
|
||||
import pkgutil
|
||||
from typing import NamedTuple, Union, Dict, Any
|
||||
from pkgutil import get_data
|
||||
from typing import Dict, Any
|
||||
|
||||
import bsdiff4
|
||||
|
||||
@@ -168,9 +167,8 @@ class TLoZWorld(World):
|
||||
# Remove map/compass check so they're always on
|
||||
# Removing a bit from the boss roars flags, so we can have more dungeon items. This allows us to
|
||||
# go past 0x1F items for dungeon items.
|
||||
base_patch_location = os.path.dirname(__file__) + "/z1_base_patch.bsdiff4"
|
||||
with open(base_patch_location, "rb") as base_patch:
|
||||
rom_data = bsdiff4.patch(rom.read(), base_patch.read())
|
||||
base_patch = get_data(__name__, os.path.join(os.path.dirname(__file__), "z1_base_patch.bsdiff4"))
|
||||
rom_data = bsdiff4.patch(rom.read(), base_patch)
|
||||
rom_data = bytearray(rom_data)
|
||||
# Set every item to the new nothing value, but keep room flags. Type 2 boss roars should
|
||||
# become type 1 boss roars, so we at least keep the sound of roaring where it should be.
|
||||
@@ -275,8 +273,10 @@ class TLoZWorld(World):
|
||||
def modify_multidata(self, multidata: dict):
|
||||
import base64
|
||||
self.rom_name_available_event.wait()
|
||||
new_name = base64.b64encode(bytes(self.rom_name)).decode()
|
||||
multidata["connect_names"][new_name] = multidata["connect_names"][self.multiworld.player_name[self.player]]
|
||||
rom_name = getattr(self, "rom_name", None)
|
||||
if rom_name:
|
||||
new_name = base64.b64encode(bytes(self.rom_name)).decode()
|
||||
multidata["connect_names"][new_name] = multidata["connect_names"][self.multiworld.player_name[self.player]]
|
||||
|
||||
def get_filler_item_name(self) -> str:
|
||||
if self.filler_items is None:
|
||||
@@ -320,4 +320,4 @@ class TLoZItem(Item):
|
||||
|
||||
|
||||
class TLoZLocation(Location):
|
||||
game = 'The Legend of Zelda'
|
||||
game = 'The Legend of Zelda'
|
||||
|
||||
@@ -463,10 +463,6 @@ class WitnessPlayerLocations:
|
||||
self.PANEL_TYPES_TO_SHUFFLE = {"General", "Laser"}
|
||||
self.CHECK_LOCATIONS = StaticWitnessLocations.GENERAL_LOCATIONS.copy()
|
||||
|
||||
if get_option_value(world, player, "puzzle_randomization") == 1:
|
||||
self.CHECK_LOCATIONS.remove("Keep Pressure Plates 4")
|
||||
self.CHECK_LOCATIONS.add("Keep Pressure Plates 2")
|
||||
|
||||
doors = get_option_value(world, player, "shuffle_doors") >= 2
|
||||
earlyutm = is_option_enabled(world, player, "early_secret_area")
|
||||
victory = get_option_value(world, player, "victory_condition")
|
||||
|
||||
Reference in New Issue
Block a user