mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-04-06 10:48:17 -07:00
Compare commits
101 Commits
revert-401
...
0-6-0-rc1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
992841a951 | ||
|
|
eb3c3d6bf2 | ||
|
|
39847c5502 | ||
|
|
130232b457 | ||
|
|
ca8ffe583d | ||
|
|
563794ab83 | ||
|
|
9443861849 | ||
|
|
cbf4bbbca8 | ||
|
|
9e353ebb8e | ||
|
|
9183e8f9c9 | ||
|
|
0bb657d2c8 | ||
|
|
992f192529 | ||
|
|
1c9409cac9 | ||
|
|
005a143e3e | ||
|
|
8732974857 | ||
|
|
1ac8349bd4 | ||
|
|
2b9fa89050 | ||
|
|
23ea3c0efc | ||
|
|
698d27aada | ||
|
|
3a46c9fd3e | ||
|
|
9507300939 | ||
|
|
0d6db291de | ||
|
|
d218dec826 | ||
|
|
3d5c277c31 | ||
|
|
a9435dc6bb | ||
|
|
8f307c226b | ||
|
|
4b8f990960 | ||
|
|
3a5a4b89ee | ||
|
|
1485882642 | ||
|
|
2e4f5a64b3 | ||
|
|
90f80ce1c1 | ||
|
|
78904151b0 | ||
|
|
9d4bd6eebd | ||
|
|
5c56dc0357 | ||
|
|
c7810823e8 | ||
|
|
902d03d447 | ||
|
|
b7621a0923 | ||
|
|
b7baaed391 | ||
|
|
9dac7d9cc3 | ||
|
|
1eefe23f11 | ||
|
|
207a76d1b5 | ||
|
|
01df35f215 | ||
|
|
bedf746f1d | ||
|
|
b91a7ac6fb | ||
|
|
79e6beeec3 | ||
|
|
dae9d4c575 | ||
|
|
04928bd83d | ||
|
|
0f3818e711 | ||
|
|
0f1dc6e19c | ||
|
|
ffd0c8b341 | ||
|
|
6220963195 | ||
|
|
20119e3162 | ||
|
|
4cb8fa3cdd | ||
|
|
93e8613da7 | ||
|
|
f9cc19e150 | ||
|
|
0f1c119c76 | ||
|
|
4c734b467f | ||
|
|
1f966ee705 | ||
|
|
172ad4e57d | ||
|
|
3f935aac13 | ||
|
|
9928639ce2 | ||
|
|
0fc722cb28 | ||
|
|
4edca0ce54 | ||
|
|
70942eda8c | ||
|
|
adcb2f59ca | ||
|
|
29b34ca9fd | ||
|
|
d97ee5d209 | ||
|
|
c2bd9df0f7 | ||
|
|
112bfe0933 | ||
|
|
96b500679d | ||
|
|
258ea10c52 | ||
|
|
043ba418ec | ||
|
|
894a8571ee | ||
|
|
874197d940 | ||
|
|
d3ed40cd4d | ||
|
|
a29ba4a6c4 | ||
|
|
fe06fe075e | ||
|
|
de58cb03da | ||
|
|
3204680662 | ||
|
|
07e896508c | ||
|
|
2d3faea713 | ||
|
|
7c89a83d19 | ||
|
|
16f8b41cb9 | ||
|
|
7d506990f5 | ||
|
|
aadcb4c903 | ||
|
|
daf94fcdb2 | ||
|
|
1cef659b78 | ||
|
|
25381ef2c2 | ||
|
|
5927926314 | ||
|
|
2a11d9fec3 | ||
|
|
82c44aaa22 | ||
|
|
a7b483e4b7 | ||
|
|
917335ec54 | ||
|
|
6e59ee2926 | ||
|
|
3c9270d802 | ||
|
|
c4bbcf9890 | ||
|
|
8dbecf3d57 | ||
|
|
0de1369ec5 | ||
|
|
fa95ae4b24 | ||
|
|
2065246186 | ||
|
|
ca1b3df45b |
16
.github/pyright-config.json
vendored
16
.github/pyright-config.json
vendored
@@ -1,8 +1,20 @@
|
||||
{
|
||||
"include": [
|
||||
"type_check.py",
|
||||
"../BizHawkClient.py",
|
||||
"../Patch.py",
|
||||
"../test/general/test_groups.py",
|
||||
"../test/general/test_helpers.py",
|
||||
"../test/general/test_memory.py",
|
||||
"../test/general/test_names.py",
|
||||
"../test/multiworld/__init__.py",
|
||||
"../test/multiworld/test_multiworlds.py",
|
||||
"../test/netutils/__init__.py",
|
||||
"../test/programs/__init__.py",
|
||||
"../test/programs/test_multi_server.py",
|
||||
"../test/utils/__init__.py",
|
||||
"../test/webhost/test_descriptions.py",
|
||||
"../worlds/AutoSNIClient.py",
|
||||
"../Patch.py"
|
||||
"type_check.py"
|
||||
],
|
||||
|
||||
"exclude": [
|
||||
|
||||
2
.github/workflows/strict-type-check.yml
vendored
2
.github/workflows/strict-type-check.yml
vendored
@@ -26,7 +26,7 @@ jobs:
|
||||
|
||||
- name: "Install dependencies"
|
||||
run: |
|
||||
python -m pip install --upgrade pip pyright==1.1.358
|
||||
python -m pip install --upgrade pip pyright==1.1.392.post0
|
||||
python ModuleUpdate.py --append "WebHostLib/requirements.txt" --force --yes
|
||||
|
||||
- name: "pyright: strict check on specific files"
|
||||
|
||||
@@ -31,6 +31,7 @@ import ssl
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
import kvui
|
||||
import argparse
|
||||
|
||||
logger = logging.getLogger("Client")
|
||||
|
||||
@@ -459,6 +460,13 @@ class CommonContext:
|
||||
await self.send_msgs([payload])
|
||||
await self.send_msgs([{"cmd": "Get", "keys": ["_read_race_mode"]}])
|
||||
|
||||
async def check_locations(self, locations: typing.Collection[int]) -> set[int]:
|
||||
"""Send new location checks to the server. Returns the set of actually new locations that were sent."""
|
||||
locations = set(locations) & self.missing_locations
|
||||
if locations:
|
||||
await self.send_msgs([{"cmd": 'LocationChecks', "locations": tuple(locations)}])
|
||||
return locations
|
||||
|
||||
async def console_input(self) -> str:
|
||||
if self.ui:
|
||||
self.ui.focus_textinput()
|
||||
@@ -1041,6 +1049,32 @@ def get_base_parser(description: typing.Optional[str] = None):
|
||||
return parser
|
||||
|
||||
|
||||
def handle_url_arg(args: "argparse.Namespace",
|
||||
parser: "typing.Optional[argparse.ArgumentParser]" = None) -> "argparse.Namespace":
|
||||
"""
|
||||
Parse the url arg "archipelago://name:pass@host:port" from launcher into correct launch args for CommonClient
|
||||
If alternate data is required the urlparse response is saved back to args.url if valid
|
||||
"""
|
||||
if not args.url:
|
||||
return args
|
||||
|
||||
url = urllib.parse.urlparse(args.url)
|
||||
if url.scheme != "archipelago":
|
||||
if not parser:
|
||||
parser = get_base_parser()
|
||||
parser.error(f"bad url, found {args.url}, expected url in form of archipelago://archipelago.gg:38281")
|
||||
return args
|
||||
|
||||
args.url = url
|
||||
args.connect = url.netloc
|
||||
if url.username:
|
||||
args.name = urllib.parse.unquote(url.username)
|
||||
if url.password:
|
||||
args.password = urllib.parse.unquote(url.password)
|
||||
|
||||
return args
|
||||
|
||||
|
||||
def run_as_textclient(*args):
|
||||
class TextContext(CommonContext):
|
||||
# Text Mode to use !hint and such with games that have no text entry
|
||||
@@ -1082,17 +1116,7 @@ def run_as_textclient(*args):
|
||||
parser.add_argument("url", nargs="?", help="Archipelago connection url")
|
||||
args = parser.parse_args(args)
|
||||
|
||||
# handle if text client is launched using the "archipelago://name:pass@host:port" url from webhost
|
||||
if args.url:
|
||||
url = urllib.parse.urlparse(args.url)
|
||||
if url.scheme == "archipelago":
|
||||
args.connect = url.netloc
|
||||
if url.username:
|
||||
args.name = urllib.parse.unquote(url.username)
|
||||
if url.password:
|
||||
args.password = urllib.parse.unquote(url.password)
|
||||
else:
|
||||
parser.error(f"bad url, found {args.url}, expected url in form of archipelago://archipelago.gg:38281")
|
||||
args = handle_url_arg(args, parser=parser)
|
||||
|
||||
# use colorama to display colored text highlighting on windows
|
||||
colorama.init()
|
||||
|
||||
23
Fill.py
23
Fill.py
@@ -531,7 +531,8 @@ def distribute_items_restrictive(multiworld: MultiWorld,
|
||||
if progitempool:
|
||||
raise FillError(
|
||||
f"Not enough locations for progression items. "
|
||||
f"There are {len(progitempool)} more progression items than there are available locations.",
|
||||
f"There are {len(progitempool)} more progression items than there are available locations.\n"
|
||||
f"Unfilled locations:\n{multiworld.get_unfilled_locations()}.",
|
||||
multiworld=multiworld,
|
||||
)
|
||||
accessibility_corrections(multiworld, multiworld.state, defaultlocations)
|
||||
@@ -570,6 +571,26 @@ def distribute_items_restrictive(multiworld: MultiWorld,
|
||||
print_data = {"items": items_counter, "locations": locations_counter}
|
||||
logging.info(f"Per-Player counts: {print_data})")
|
||||
|
||||
more_locations = locations_counter - items_counter
|
||||
more_items = items_counter - locations_counter
|
||||
for player in multiworld.player_ids:
|
||||
if more_locations[player]:
|
||||
logging.error(
|
||||
f"Player {multiworld.get_player_name(player)} had {more_locations[player]} more locations than items.")
|
||||
elif more_items[player]:
|
||||
logging.warning(
|
||||
f"Player {multiworld.get_player_name(player)} had {more_items[player]} more items than locations.")
|
||||
if unfilled:
|
||||
raise FillError(
|
||||
f"Unable to fill all locations.\n" +
|
||||
f"Unfilled locations({len(unfilled)}): {unfilled}"
|
||||
)
|
||||
else:
|
||||
logging.warning(
|
||||
f"Unable to place all items.\n" +
|
||||
f"Unplaced items({len(unplaced)}): {unplaced}"
|
||||
)
|
||||
|
||||
|
||||
def flood_items(multiworld: MultiWorld) -> None:
|
||||
# get items to distribute
|
||||
|
||||
24
Generate.py
24
Generate.py
@@ -42,7 +42,9 @@ def mystery_argparse():
|
||||
help="Path to output folder. Absolute or relative to cwd.") # absolute or relative to cwd
|
||||
parser.add_argument('--race', action='store_true', default=defaults.race)
|
||||
parser.add_argument('--meta_file_path', default=defaults.meta_file_path)
|
||||
parser.add_argument('--log_level', default='info', help='Sets log level')
|
||||
parser.add_argument('--log_level', default=defaults.loglevel, help='Sets log level')
|
||||
parser.add_argument('--log_time', help="Add timestamps to STDOUT",
|
||||
default=defaults.logtime, action='store_true')
|
||||
parser.add_argument("--csv_output", action="store_true",
|
||||
help="Output rolled player options to csv (made for async multiworld).")
|
||||
parser.add_argument("--plando", default=defaults.plando_options,
|
||||
@@ -75,7 +77,7 @@ def main(args=None) -> Tuple[argparse.Namespace, int]:
|
||||
|
||||
seed = get_seed(args.seed)
|
||||
|
||||
Utils.init_logging(f"Generate_{seed}", loglevel=args.log_level)
|
||||
Utils.init_logging(f"Generate_{seed}", loglevel=args.log_level, add_timestamp=args.log_time)
|
||||
random.seed(seed)
|
||||
seed_name = get_seed_name(random)
|
||||
|
||||
@@ -438,7 +440,7 @@ def roll_settings(weights: dict, plando_options: PlandoOptions = PlandoOptions.b
|
||||
if "linked_options" in weights:
|
||||
weights = roll_linked_options(weights)
|
||||
|
||||
valid_keys = set()
|
||||
valid_keys = {"triggers"}
|
||||
if "triggers" in weights:
|
||||
weights = roll_triggers(weights, weights["triggers"], valid_keys)
|
||||
|
||||
@@ -497,15 +499,23 @@ def roll_settings(weights: dict, plando_options: PlandoOptions = PlandoOptions.b
|
||||
for option_key, option in world_type.options_dataclass.type_hints.items():
|
||||
handle_option(ret, game_weights, option_key, option, plando_options)
|
||||
valid_keys.add(option_key)
|
||||
for option_key in game_weights:
|
||||
if option_key in {"triggers", *valid_keys}:
|
||||
continue
|
||||
logging.warning(f"{option_key} is not a valid option name for {ret.game} and is not present in triggers.")
|
||||
|
||||
# TODO remove plando_items after moving it to the options system
|
||||
valid_keys.add("plando_items")
|
||||
if PlandoOptions.items in plando_options:
|
||||
ret.plando_items = copy.deepcopy(game_weights.get("plando_items", []))
|
||||
if ret.game == "A Link to the Past":
|
||||
# TODO there are still more LTTP options not on the options system
|
||||
valid_keys |= {"sprite_pool", "sprite", "random_sprite_on_event"}
|
||||
roll_alttp_settings(ret, game_weights)
|
||||
|
||||
# log a warning for options within a game section that aren't determined as valid
|
||||
for option_key in game_weights:
|
||||
if option_key in valid_keys:
|
||||
continue
|
||||
logging.warning(f"{option_key} is not a valid option name for {ret.game} and is not present in triggers "
|
||||
f"for player {ret.name}.")
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
|
||||
2
LICENSE
2
LICENSE
@@ -1,7 +1,7 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2017 LLCoolDave
|
||||
Copyright (c) 2022 Berserker66
|
||||
Copyright (c) 2025 Berserker66
|
||||
Copyright (c) 2022 CaitSith2
|
||||
Copyright (c) 2021 LegendaryLinux
|
||||
|
||||
|
||||
@@ -560,6 +560,10 @@ class LinksAwakeningContext(CommonContext):
|
||||
|
||||
while self.client.auth == None:
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
# Just return if we're closing
|
||||
if self.exit_event.is_set():
|
||||
return
|
||||
self.auth = self.client.auth
|
||||
await self.send_connect()
|
||||
|
||||
|
||||
@@ -444,7 +444,7 @@ class Context:
|
||||
|
||||
self.slot_info = decoded_obj["slot_info"]
|
||||
self.games = {slot: slot_info.game for slot, slot_info in self.slot_info.items()}
|
||||
self.groups = {slot: slot_info.group_members for slot, slot_info in self.slot_info.items()
|
||||
self.groups = {slot: set(slot_info.group_members) for slot, slot_info in self.slot_info.items()
|
||||
if slot_info.type == SlotType.group}
|
||||
|
||||
self.clients = {0: {}}
|
||||
@@ -743,16 +743,17 @@ class Context:
|
||||
concerns[player].append(data)
|
||||
if not hint.local and data not in concerns[hint.finding_player]:
|
||||
concerns[hint.finding_player].append(data)
|
||||
# remember hints in all cases
|
||||
|
||||
# since hints are bidirectional, finding player and receiving player,
|
||||
# we can check once if hint already exists
|
||||
if hint not in self.hints[team, hint.finding_player]:
|
||||
self.hints[team, hint.finding_player].add(hint)
|
||||
new_hint_events.add(hint.finding_player)
|
||||
for player in self.slot_set(hint.receiving_player):
|
||||
self.hints[team, player].add(hint)
|
||||
new_hint_events.add(player)
|
||||
# only remember hints that were not already found at the time of creation
|
||||
if not hint.found:
|
||||
# since hints are bidirectional, finding player and receiving player,
|
||||
# we can check once if hint already exists
|
||||
if hint not in self.hints[team, hint.finding_player]:
|
||||
self.hints[team, hint.finding_player].add(hint)
|
||||
new_hint_events.add(hint.finding_player)
|
||||
for player in self.slot_set(hint.receiving_player):
|
||||
self.hints[team, player].add(hint)
|
||||
new_hint_events.add(player)
|
||||
|
||||
self.logger.info("Notice (Team #%d): %s" % (team + 1, format_hint(self, team, hint)))
|
||||
for slot in new_hint_events:
|
||||
@@ -1887,7 +1888,8 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
|
||||
for location in args["locations"]:
|
||||
if type(location) is not int:
|
||||
await ctx.send_msgs(client,
|
||||
[{'cmd': 'InvalidPacket', "type": "arguments", "text": 'LocationScouts',
|
||||
[{'cmd': 'InvalidPacket', "type": "arguments",
|
||||
"text": 'Locations has to be a list of integers',
|
||||
"original_cmd": cmd}])
|
||||
return
|
||||
|
||||
@@ -1990,6 +1992,7 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
|
||||
args["cmd"] = "SetReply"
|
||||
value = ctx.stored_data.get(args["key"], args.get("default", 0))
|
||||
args["original_value"] = copy.copy(value)
|
||||
args["slot"] = client.slot
|
||||
for operation in args["operations"]:
|
||||
func = modify_functions[operation["operation"]]
|
||||
value = func(value, operation["value"])
|
||||
|
||||
@@ -10,7 +10,7 @@ import websockets
|
||||
from Utils import ByValue, Version
|
||||
|
||||
|
||||
class HintStatus(enum.IntEnum):
|
||||
class HintStatus(ByValue, enum.IntEnum):
|
||||
HINT_FOUND = 0
|
||||
HINT_UNSPECIFIED = 1
|
||||
HINT_NO_PRIORITY = 10
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import tkinter as tk
|
||||
import argparse
|
||||
import logging
|
||||
import random
|
||||
import os
|
||||
import zipfile
|
||||
from itertools import chain
|
||||
@@ -197,7 +196,6 @@ def set_icon(window):
|
||||
def adjust(args):
|
||||
# Create a fake multiworld and OOTWorld to use as a base
|
||||
multiworld = MultiWorld(1)
|
||||
multiworld.per_slot_randoms = {1: random}
|
||||
ootworld = OOTWorld(multiworld, 1)
|
||||
# Set options in the fake OOTWorld
|
||||
for name, option in chain(cosmetic_options.items(), sfx_options.items()):
|
||||
|
||||
22
Options.py
22
Options.py
@@ -137,7 +137,7 @@ class Option(typing.Generic[T], metaclass=AssembleOptions):
|
||||
If this is False, the docstring is instead interpreted as plain text, and
|
||||
displayed as-is on the WebHost with whitespace preserved.
|
||||
|
||||
If this is None, it inherits the value of `World.rich_text_options_doc`. For
|
||||
If this is None, it inherits the value of `WebWorld.rich_text_options_doc`. For
|
||||
backwards compatibility, this defaults to False, but worlds are encouraged to
|
||||
set it to True and use reStructuredText for their Option documentation.
|
||||
|
||||
@@ -689,9 +689,9 @@ class Range(NumericOption):
|
||||
@classmethod
|
||||
def weighted_range(cls, text) -> Range:
|
||||
if text == "random-low":
|
||||
return cls(cls.triangular(cls.range_start, cls.range_end, cls.range_start))
|
||||
return cls(cls.triangular(cls.range_start, cls.range_end, 0.0))
|
||||
elif text == "random-high":
|
||||
return cls(cls.triangular(cls.range_start, cls.range_end, cls.range_end))
|
||||
return cls(cls.triangular(cls.range_start, cls.range_end, 1.0))
|
||||
elif text == "random-middle":
|
||||
return cls(cls.triangular(cls.range_start, cls.range_end))
|
||||
elif text.startswith("random-range-"):
|
||||
@@ -717,11 +717,11 @@ class Range(NumericOption):
|
||||
f"{random_range[0]}-{random_range[1]} is outside allowed range "
|
||||
f"{cls.range_start}-{cls.range_end} for option {cls.__name__}")
|
||||
if text.startswith("random-range-low"):
|
||||
return cls(cls.triangular(random_range[0], random_range[1], random_range[0]))
|
||||
return cls(cls.triangular(random_range[0], random_range[1], 0.0))
|
||||
elif text.startswith("random-range-middle"):
|
||||
return cls(cls.triangular(random_range[0], random_range[1]))
|
||||
elif text.startswith("random-range-high"):
|
||||
return cls(cls.triangular(random_range[0], random_range[1], random_range[1]))
|
||||
return cls(cls.triangular(random_range[0], random_range[1], 1.0))
|
||||
else:
|
||||
return cls(random.randint(random_range[0], random_range[1]))
|
||||
|
||||
@@ -739,8 +739,16 @@ class Range(NumericOption):
|
||||
return str(self.value)
|
||||
|
||||
@staticmethod
|
||||
def triangular(lower: int, end: int, tri: typing.Optional[int] = None) -> int:
|
||||
return int(round(random.triangular(lower, end, tri), 0))
|
||||
def triangular(lower: int, end: int, tri: float = 0.5) -> int:
|
||||
"""
|
||||
Integer triangular distribution for `lower` inclusive to `end` inclusive.
|
||||
|
||||
Expects `lower <= end` and `0.0 <= tri <= 1.0`. The result of other inputs is undefined.
|
||||
"""
|
||||
# Use the continuous range [lower, end + 1) to produce an integer result in [lower, end].
|
||||
# random.triangular is actually [a, b] and not [a, b), so there is a very small chance of getting exactly b even
|
||||
# when a != b, so ensure the result is never more than `end`.
|
||||
return min(end, math.floor(random.triangular(0.0, 1.0, tri) * (end - lower + 1) + lower))
|
||||
|
||||
|
||||
class NamedRange(Range):
|
||||
|
||||
@@ -243,6 +243,9 @@ class SNIContext(CommonContext):
|
||||
# Once the games handled by SNIClient gets made to be remote items,
|
||||
# this will no longer be needed.
|
||||
async_start(self.send_msgs([{"cmd": "LocationScouts", "locations": list(new_locations)}]))
|
||||
|
||||
if self.client_handler is not None:
|
||||
self.client_handler.on_package(self, cmd, args)
|
||||
|
||||
def run_gui(self) -> None:
|
||||
from kvui import GameManager
|
||||
|
||||
27
Utils.py
27
Utils.py
@@ -152,8 +152,15 @@ def home_path(*path: str) -> str:
|
||||
if hasattr(home_path, 'cached_path'):
|
||||
pass
|
||||
elif sys.platform.startswith('linux'):
|
||||
home_path.cached_path = os.path.expanduser('~/Archipelago')
|
||||
os.makedirs(home_path.cached_path, 0o700, exist_ok=True)
|
||||
xdg_data_home = os.getenv('XDG_DATA_HOME', os.path.expanduser('~/.local/share'))
|
||||
home_path.cached_path = xdg_data_home + '/Archipelago'
|
||||
if not os.path.isdir(home_path.cached_path):
|
||||
legacy_home_path = os.path.expanduser('~/Archipelago')
|
||||
if os.path.isdir(legacy_home_path):
|
||||
os.renames(legacy_home_path, home_path.cached_path)
|
||||
os.symlink(home_path.cached_path, legacy_home_path)
|
||||
else:
|
||||
os.makedirs(home_path.cached_path, 0o700, exist_ok=True)
|
||||
else:
|
||||
# not implemented
|
||||
home_path.cached_path = local_path() # this will generate the same exceptions we got previously
|
||||
@@ -514,8 +521,8 @@ def init_logging(name: str, loglevel: typing.Union[str, int] = logging.INFO,
|
||||
def filter(self, record: logging.LogRecord) -> bool:
|
||||
return self.condition(record)
|
||||
|
||||
file_handler.addFilter(Filter("NoStream", lambda record: not getattr(record, "NoFile", False)))
|
||||
file_handler.addFilter(Filter("NoCarriageReturn", lambda record: '\r' not in record.msg))
|
||||
file_handler.addFilter(Filter("NoStream", lambda record: not getattr(record, "NoFile", False)))
|
||||
file_handler.addFilter(Filter("NoCarriageReturn", lambda record: '\r' not in record.getMessage()))
|
||||
root_logger.addHandler(file_handler)
|
||||
if sys.stdout:
|
||||
formatter = logging.Formatter(fmt='[%(asctime)s] %(message)s', datefmt='%Y-%m-%d %H:%M:%S')
|
||||
@@ -933,7 +940,7 @@ def freeze_support() -> None:
|
||||
|
||||
def visualize_regions(root_region: Region, file_name: str, *,
|
||||
show_entrance_names: bool = False, show_locations: bool = True, show_other_regions: bool = True,
|
||||
linetype_ortho: bool = True) -> None:
|
||||
linetype_ortho: bool = True, regions_to_highlight: set[Region] | None = None) -> None:
|
||||
"""Visualize the layout of a world as a PlantUML diagram.
|
||||
|
||||
:param root_region: The region from which to start the diagram from. (Usually the "Menu" region of your world.)
|
||||
@@ -949,16 +956,22 @@ def visualize_regions(root_region: Region, file_name: str, *,
|
||||
Items without ID will be shown in italics.
|
||||
:param show_other_regions: (default True) If enabled, regions that can't be reached by traversing exits are shown.
|
||||
:param linetype_ortho: (default True) If enabled, orthogonal straight line parts will be used; otherwise polylines.
|
||||
:param regions_to_highlight: Regions that will be highlighted in green if they are reachable.
|
||||
|
||||
Example usage in World code:
|
||||
from Utils import visualize_regions
|
||||
visualize_regions(self.multiworld.get_region("Menu", self.player), "my_world.puml")
|
||||
state = self.multiworld.get_all_state(False)
|
||||
state.update_reachable_regions(self.player)
|
||||
visualize_regions(self.get_region("Menu"), "my_world.puml", show_entrance_names=True,
|
||||
regions_to_highlight=state.reachable_regions[self.player])
|
||||
|
||||
Example usage in Main code:
|
||||
from Utils import visualize_regions
|
||||
for player in multiworld.player_ids:
|
||||
visualize_regions(multiworld.get_region("Menu", player), f"{multiworld.get_out_file_name_base(player)}.puml")
|
||||
"""
|
||||
if regions_to_highlight is None:
|
||||
regions_to_highlight = set()
|
||||
assert root_region.multiworld, "The multiworld attribute of root_region has to be filled"
|
||||
from BaseClasses import Entrance, Item, Location, LocationProgressType, MultiWorld, Region
|
||||
from collections import deque
|
||||
@@ -1011,7 +1024,7 @@ def visualize_regions(root_region: Region, file_name: str, *,
|
||||
uml.append(f"\"{fmt(region)}\" : {{field}} {lock}{fmt(location)}")
|
||||
|
||||
def visualize_region(region: Region) -> None:
|
||||
uml.append(f"class \"{fmt(region)}\"")
|
||||
uml.append(f"class \"{fmt(region)}\" {'#00FF00' if region in regions_to_highlight else ''}")
|
||||
if show_locations:
|
||||
visualize_locations(region)
|
||||
visualize_exits(region)
|
||||
|
||||
@@ -3,13 +3,13 @@ from typing import List, Tuple
|
||||
|
||||
from flask import Blueprint
|
||||
|
||||
from ..models import Seed
|
||||
from ..models import Seed, Slot
|
||||
|
||||
api_endpoints = Blueprint('api', __name__, url_prefix="/api")
|
||||
|
||||
|
||||
def get_players(seed: Seed) -> List[Tuple[str, str]]:
|
||||
return [(slot.player_name, slot.game) for slot in seed.slots]
|
||||
return [(slot.player_name, slot.game) for slot in seed.slots.order_by(Slot.player_id)]
|
||||
|
||||
|
||||
from . import datapackage, generate, room, user # trigger registration
|
||||
|
||||
@@ -30,4 +30,4 @@ def get_seeds():
|
||||
"creation_time": seed.creation_time,
|
||||
"players": get_players(seed.slots),
|
||||
})
|
||||
return jsonify(response)
|
||||
return jsonify(response)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{% block footer %}
|
||||
<footer id="island-footer">
|
||||
<div id="copyright-notice">Copyright 2024 Archipelago</div>
|
||||
<div id="copyright-notice">Copyright 2025 Archipelago</div>
|
||||
<div id="links">
|
||||
<a href="/sitemap">Site Map</a>
|
||||
-
|
||||
|
||||
@@ -147,3 +147,8 @@
|
||||
rectangle: self.x-2, self.y-2, self.width+4, self.height+4
|
||||
<ServerToolTip>:
|
||||
pos_hint: {'center_y': 0.5, 'center_x': 0.5}
|
||||
<AutocompleteHintInput>
|
||||
size_hint_y: None
|
||||
height: dp(30)
|
||||
multiline: False
|
||||
write_tab: False
|
||||
|
||||
@@ -121,6 +121,14 @@ Response:
|
||||
|
||||
Expected Response Type: `HASH_RESPONSE`
|
||||
|
||||
- `MEMORY_SIZE`
|
||||
Returns the size in bytes of the specified memory domain.
|
||||
|
||||
Expected Response Type: `MEMORY_SIZE_RESPONSE`
|
||||
|
||||
Additional Fields:
|
||||
- `domain` (`string`): The name of the memory domain to check
|
||||
|
||||
- `GUARD`
|
||||
Checks a section of memory against `expected_data`. If the bytes starting
|
||||
at `address` do not match `expected_data`, the response will have `value`
|
||||
@@ -216,6 +224,12 @@ Response:
|
||||
Additional Fields:
|
||||
- `value` (`string`): The returned hash
|
||||
|
||||
- `MEMORY_SIZE_RESPONSE`
|
||||
Contains the size in bytes of the specified memory domain.
|
||||
|
||||
Additional Fields:
|
||||
- `value` (`number`): The size of the domain in bytes
|
||||
|
||||
- `GUARD_RESPONSE`
|
||||
The result of an attempted `GUARD` request.
|
||||
|
||||
@@ -376,6 +390,15 @@ request_handlers = {
|
||||
return res
|
||||
end,
|
||||
|
||||
["MEMORY_SIZE"] = function (req)
|
||||
local res = {}
|
||||
|
||||
res["type"] = "MEMORY_SIZE_RESPONSE"
|
||||
res["value"] = memory.getmemorydomainsize(req["domain"])
|
||||
|
||||
return res
|
||||
end,
|
||||
|
||||
["GUARD"] = function (req)
|
||||
local res = {}
|
||||
local expected_data = base64.decode(req["expected_data"])
|
||||
@@ -613,9 +636,11 @@ end)
|
||||
|
||||
if bizhawk_major < 2 or (bizhawk_major == 2 and bizhawk_minor < 7) then
|
||||
print("Must use BizHawk 2.7.0 or newer")
|
||||
elseif bizhawk_major > 2 or (bizhawk_major == 2 and bizhawk_minor > 9) then
|
||||
print("Warning: This version of BizHawk is newer than this script. If it doesn't work, consider downgrading to 2.9.")
|
||||
else
|
||||
if bizhawk_major > 2 or (bizhawk_major == 2 and bizhawk_minor > 10) then
|
||||
print("Warning: This version of BizHawk is newer than this script. If it doesn't work, consider downgrading to 2.10.")
|
||||
end
|
||||
|
||||
if emu.getsystemid() == "NULL" then
|
||||
print("No ROM is loaded. Please load a ROM.")
|
||||
while emu.getsystemid() == "NULL" do
|
||||
|
||||
@@ -1816,7 +1816,7 @@ end
|
||||
|
||||
-- Main control handling: main loop and socket receive
|
||||
|
||||
function receive()
|
||||
function APreceive()
|
||||
l, e = ootSocket:receive()
|
||||
-- Handle incoming message
|
||||
if e == 'closed' then
|
||||
@@ -1874,7 +1874,7 @@ function main()
|
||||
end
|
||||
if (curstate == STATE_OK) or (curstate == STATE_INITIAL_CONNECTION_MADE) or (curstate == STATE_TENTATIVELY_CONNECTED) then
|
||||
if (frame % 30 == 0) then
|
||||
receive()
|
||||
APreceive()
|
||||
end
|
||||
elseif (curstate == STATE_UNINITIALIZED) then
|
||||
if (frame % 60 == 0) then
|
||||
|
||||
@@ -99,6 +99,9 @@
|
||||
# Lingo
|
||||
/worlds/lingo/ @hatkirby
|
||||
|
||||
# Links Awakening DX
|
||||
/worlds/ladx/ @threeandthreee
|
||||
|
||||
# Lufia II Ancient Cave
|
||||
/worlds/lufia2ac/ @el-u
|
||||
/worlds/lufia2ac/docs/ @wordfcuk @el-u
|
||||
@@ -152,7 +155,7 @@
|
||||
/worlds/saving_princess/ @LeonarthCG
|
||||
|
||||
# Shivers
|
||||
/worlds/shivers/ @GodlFire
|
||||
/worlds/shivers/ @GodlFire @korydondzila
|
||||
|
||||
# A Short Hike
|
||||
/worlds/shorthike/ @chandler05 @BrandenEK
|
||||
@@ -236,9 +239,6 @@
|
||||
# Final Fantasy (1)
|
||||
# /worlds/ff1/
|
||||
|
||||
# Links Awakening DX
|
||||
# /worlds/ladx/
|
||||
|
||||
# Ocarina of Time
|
||||
# /worlds/oot/
|
||||
|
||||
|
||||
@@ -261,6 +261,7 @@ Sent to clients in response to a [Set](#Set) package if want_reply was set to tr
|
||||
| key | str | The key that was updated. |
|
||||
| value | any | The new value for the key. |
|
||||
| original_value | any | The value the key had before it was updated. Not present on "_read" prefixed special keys. |
|
||||
| slot | int | The slot that originally sent the Set package causing this change. |
|
||||
|
||||
Additional arguments added to the [Set](#Set) package that triggered this [SetReply](#SetReply) will also be passed along.
|
||||
|
||||
|
||||
@@ -95,7 +95,7 @@ user hovers over the yellow "(?)" icon, and included in the YAML templates gener
|
||||
The WebHost can display Option documentation either as plain text with all whitespace preserved (other than the base
|
||||
indentation), or as HTML generated from the standard Python [reStructuredText] format. Although plain text is the
|
||||
default for backwards compatibility, world authors are encouraged to write their Option documentation as
|
||||
reStructuredText and enable rich text rendering by setting `World.rich_text_options_doc = True`.
|
||||
reStructuredText and enable rich text rendering by setting `WebWorld.rich_text_options_doc = True`.
|
||||
|
||||
[reStructuredText]: https://docutils.sourceforge.io/rst.html
|
||||
|
||||
|
||||
65
kvui.py
65
kvui.py
@@ -40,7 +40,7 @@ from kivy.core.image import ImageLoader, ImageLoaderBase, ImageData
|
||||
from kivy.base import ExceptionHandler, ExceptionManager
|
||||
from kivy.clock import Clock
|
||||
from kivy.factory import Factory
|
||||
from kivy.properties import BooleanProperty, ObjectProperty
|
||||
from kivy.properties import BooleanProperty, ObjectProperty, NumericProperty
|
||||
from kivy.metrics import dp
|
||||
from kivy.effects.scroll import ScrollEffect
|
||||
from kivy.uix.widget import Widget
|
||||
@@ -64,6 +64,7 @@ from kivy.uix.recycleboxlayout import RecycleBoxLayout
|
||||
from kivy.uix.recycleview.layout import LayoutSelectionBehavior
|
||||
from kivy.animation import Animation
|
||||
from kivy.uix.popup import Popup
|
||||
from kivy.uix.dropdown import DropDown
|
||||
from kivy.uix.image import AsyncImage
|
||||
|
||||
fade_in_animation = Animation(opacity=0, duration=0) + Animation(opacity=1, duration=0.25)
|
||||
@@ -305,6 +306,50 @@ class SelectableLabel(RecycleDataViewBehavior, TooltipLabel):
|
||||
""" Respond to the selection of items in the view. """
|
||||
self.selected = is_selected
|
||||
|
||||
|
||||
class AutocompleteHintInput(TextInput):
|
||||
min_chars = NumericProperty(3)
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self.dropdown = DropDown()
|
||||
self.dropdown.bind(on_select=lambda instance, x: setattr(self, 'text', x))
|
||||
self.bind(on_text_validate=self.on_message)
|
||||
|
||||
def on_message(self, instance):
|
||||
App.get_running_app().commandprocessor("!hint "+instance.text)
|
||||
|
||||
def on_text(self, instance, value):
|
||||
if len(value) >= self.min_chars:
|
||||
self.dropdown.clear_widgets()
|
||||
ctx: context_type = App.get_running_app().ctx
|
||||
if not ctx.game:
|
||||
return
|
||||
item_names = ctx.item_names._game_store[ctx.game].values()
|
||||
|
||||
def on_press(button: Button):
|
||||
split_text = MarkupLabel(text=button.text).markup
|
||||
return self.dropdown.select("".join(text_frag for text_frag in split_text
|
||||
if not text_frag.startswith("[")))
|
||||
lowered = value.lower()
|
||||
for item_name in item_names:
|
||||
try:
|
||||
index = item_name.lower().index(lowered)
|
||||
except ValueError:
|
||||
pass # substring not found
|
||||
else:
|
||||
text = escape_markup(item_name)
|
||||
text = text[:index] + "[b]" + text[index:index+len(value)]+"[/b]"+text[index+len(value):]
|
||||
btn = Button(text=text, size_hint_y=None, height=dp(30), markup=True)
|
||||
btn.bind(on_release=on_press)
|
||||
self.dropdown.add_widget(btn)
|
||||
if not self.dropdown.attach_to:
|
||||
self.dropdown.open(self)
|
||||
else:
|
||||
self.dropdown.dismiss()
|
||||
|
||||
|
||||
class HintLabel(RecycleDataViewBehavior, BoxLayout):
|
||||
selected = BooleanProperty(False)
|
||||
striped = BooleanProperty(False)
|
||||
@@ -570,8 +615,10 @@ class GameManager(App):
|
||||
# show Archipelago tab if other logging is present
|
||||
self.tabs.add_widget(panel)
|
||||
|
||||
hint_panel = self.add_client_tab("Hints", HintLog(self.json_to_kivy_parser))
|
||||
hint_panel = self.add_client_tab("Hints", HintLayout())
|
||||
self.hint_log = HintLog(self.json_to_kivy_parser)
|
||||
self.log_panels["Hints"] = hint_panel.content
|
||||
hint_panel.content.add_widget(self.hint_log)
|
||||
|
||||
if len(self.logging_pairs) == 1:
|
||||
self.tabs.default_tab_text = "Archipelago"
|
||||
@@ -698,7 +745,7 @@ class GameManager(App):
|
||||
|
||||
def update_hints(self):
|
||||
hints = self.ctx.stored_data.get(f"_read_hints_{self.ctx.team}_{self.ctx.slot}", [])
|
||||
self.log_panels["Hints"].refresh_hints(hints)
|
||||
self.hint_log.refresh_hints(hints)
|
||||
|
||||
# default F1 keybind, opens a settings menu, that seems to break the layout engine once closed
|
||||
def open_settings(self, *largs):
|
||||
@@ -753,6 +800,17 @@ class UILog(RecycleView):
|
||||
element.height = element.texture_size[1]
|
||||
|
||||
|
||||
class HintLayout(BoxLayout):
|
||||
orientation = "vertical"
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
boxlayout = BoxLayout(orientation="horizontal", size_hint_y=None, height=dp(30))
|
||||
boxlayout.add_widget(Label(text="New Hint:", size_hint_x=None, size_hint_y=None, height=dp(30)))
|
||||
boxlayout.add_widget(AutocompleteHintInput())
|
||||
self.add_widget(boxlayout)
|
||||
|
||||
|
||||
status_names: typing.Dict[HintStatus, str] = {
|
||||
HintStatus.HINT_FOUND: "Found",
|
||||
HintStatus.HINT_UNSPECIFIED: "Unspecified",
|
||||
@@ -769,6 +827,7 @@ status_colors: typing.Dict[HintStatus, str] = {
|
||||
}
|
||||
|
||||
|
||||
|
||||
class HintLog(RecycleView):
|
||||
header = {
|
||||
"receiving": {"text": "[u]Receiving Player[/u]"},
|
||||
|
||||
@@ -2,3 +2,6 @@
|
||||
python_files = test_*.py Test*.py # TODO: remove Test* once all worlds have been ported
|
||||
python_classes = Test
|
||||
python_functions = test
|
||||
testpaths =
|
||||
test
|
||||
worlds
|
||||
|
||||
@@ -7,7 +7,7 @@ schema>=0.7.7
|
||||
kivy>=2.3.0
|
||||
bsdiff4>=1.2.4
|
||||
platformdirs>=4.2.2
|
||||
certifi>=2024.8.30
|
||||
certifi>=2024.12.14
|
||||
cython>=3.0.11
|
||||
cymem>=2.0.8
|
||||
orjson>=3.10.7
|
||||
|
||||
@@ -678,6 +678,8 @@ class GeneratorOptions(Group):
|
||||
race: Race = Race(0)
|
||||
plando_options: PlandoOptions = PlandoOptions("bosses, connections, texts")
|
||||
panic_method: PanicMethod = PanicMethod("swap")
|
||||
loglevel: str = "info"
|
||||
logtime: bool = False
|
||||
|
||||
|
||||
class SNIOptions(Group):
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import unittest
|
||||
from typing import Callable, Dict, Optional
|
||||
|
||||
from typing_extensions import override
|
||||
|
||||
from BaseClasses import CollectionState, MultiWorld, Region
|
||||
|
||||
|
||||
@@ -8,6 +10,7 @@ class TestHelpers(unittest.TestCase):
|
||||
multiworld: MultiWorld
|
||||
player: int = 1
|
||||
|
||||
@override
|
||||
def setUp(self) -> None:
|
||||
self.multiworld = MultiWorld(self.player)
|
||||
self.multiworld.game[self.player] = "helper_test_game"
|
||||
@@ -38,15 +41,15 @@ class TestHelpers(unittest.TestCase):
|
||||
"TestRegion1": {"TestRegion2": "connection"},
|
||||
"TestRegion2": {"TestRegion1": None},
|
||||
}
|
||||
|
||||
|
||||
reg_exit_set: Dict[str, set[str]] = {
|
||||
"TestRegion1": {"TestRegion3"}
|
||||
}
|
||||
|
||||
|
||||
exit_rules: Dict[str, Callable[[CollectionState], bool]] = {
|
||||
"TestRegion1": lambda state: state.has("test_item", self.player)
|
||||
}
|
||||
|
||||
|
||||
self.multiworld.regions += [Region(region, self.player, self.multiworld, regions[region]) for region in regions]
|
||||
|
||||
with self.subTest("Test Location Creation Helper"):
|
||||
@@ -73,7 +76,7 @@ class TestHelpers(unittest.TestCase):
|
||||
entrance_name = exit_name if exit_name else f"{parent} -> {exit_reg}"
|
||||
self.assertEqual(exit_rules[exit_reg],
|
||||
self.multiworld.get_entrance(entrance_name, self.player).access_rule)
|
||||
|
||||
|
||||
for region in reg_exit_set:
|
||||
current_region = self.multiworld.get_region(region, self.player)
|
||||
current_region.add_exits(reg_exit_set[region])
|
||||
|
||||
@@ -39,7 +39,7 @@ class TestImplemented(unittest.TestCase):
|
||||
"""Tests that if a world creates slot data, it's json serializable."""
|
||||
for game_name, world_type in AutoWorldRegister.world_types.items():
|
||||
# has an await for generate_output which isn't being called
|
||||
if game_name in {"Ocarina of Time", "Zillion"}:
|
||||
if game_name in {"Ocarina of Time"}:
|
||||
continue
|
||||
multiworld = setup_solo_multiworld(world_type)
|
||||
with self.subTest(game=game_name, seed=multiworld.seed):
|
||||
@@ -117,3 +117,12 @@ class TestImplemented(unittest.TestCase):
|
||||
f"\nUnexpectedly reachable locations in sphere {sphere_num}:"
|
||||
f"\n{reachable_only_with_explicit}")
|
||||
self.fail("Unreachable")
|
||||
|
||||
def test_no_items_or_locations_or_regions_submitted_in_init(self):
|
||||
"""Test that worlds don't submit items/locations/regions to the multiworld in __init__"""
|
||||
for game_name, world_type in AutoWorldRegister.world_types.items():
|
||||
with self.subTest("Game", game=game_name):
|
||||
multiworld = setup_solo_multiworld(world_type, ())
|
||||
self.assertEqual(len(multiworld.itempool), 0)
|
||||
self.assertEqual(len(multiworld.get_locations()), 0)
|
||||
self.assertEqual(len(multiworld.get_regions()), 0)
|
||||
|
||||
@@ -5,7 +5,7 @@ from . import setup_solo_multiworld
|
||||
|
||||
|
||||
class TestWorldMemory(unittest.TestCase):
|
||||
def test_leak(self):
|
||||
def test_leak(self) -> None:
|
||||
"""Tests that worlds don't leak references to MultiWorld or themselves with default options."""
|
||||
import gc
|
||||
import weakref
|
||||
|
||||
@@ -3,7 +3,7 @@ from worlds.AutoWorld import AutoWorldRegister
|
||||
|
||||
|
||||
class TestNames(unittest.TestCase):
|
||||
def test_item_names_format(self):
|
||||
def test_item_names_format(self) -> None:
|
||||
"""Item names must not be all numeric in order to differentiate between ID and name in !hint"""
|
||||
for gamename, world_type in AutoWorldRegister.world_types.items():
|
||||
with self.subTest(game=gamename):
|
||||
@@ -11,7 +11,7 @@ class TestNames(unittest.TestCase):
|
||||
self.assertFalse(item_name.isnumeric(),
|
||||
f"Item name \"{item_name}\" is invalid. It must not be numeric.")
|
||||
|
||||
def test_location_name_format(self):
|
||||
def test_location_name_format(self) -> None:
|
||||
"""Location names must not be all numeric in order to differentiate between ID and name in !hint_location"""
|
||||
for gamename, world_type in AutoWorldRegister.world_types.items():
|
||||
with self.subTest(game=gamename):
|
||||
|
||||
@@ -86,3 +86,7 @@ class SNIClient(abc.ABC, metaclass=AutoSNIClientRegister):
|
||||
async def deathlink_kill_player(self, ctx: SNIContext) -> None:
|
||||
""" override this with implementation to kill player """
|
||||
pass
|
||||
|
||||
def on_package(self, ctx: SNIContext, cmd: str, args: Dict[str, Any]) -> None:
|
||||
""" override this with code to handle packages from the server """
|
||||
pass
|
||||
|
||||
@@ -18,16 +18,42 @@ class Type(Enum):
|
||||
|
||||
|
||||
class Component:
|
||||
"""
|
||||
A Component represents a process launchable by Archipelago Launcher, either by a User action in the GUI,
|
||||
by resolving an archipelago://user:pass@host:port link from the WebHost, by resolving a patch file's metadata,
|
||||
or by using a component name arg while running the Launcher in CLI i.e. `ArchipelagoLauncher.exe "Text Client"`
|
||||
|
||||
Expected to be appended to LauncherComponents.component list to be used.
|
||||
"""
|
||||
display_name: str
|
||||
"""Used as the GUI button label and the component name in the CLI args"""
|
||||
type: Type
|
||||
"""
|
||||
Enum "Type" classification of component intent, for filtering in the Launcher GUI
|
||||
If not set in the constructor, it will be inferred by display_name
|
||||
"""
|
||||
script_name: Optional[str]
|
||||
"""Recommended to use func instead; Name of file to run when the component is called"""
|
||||
frozen_name: Optional[str]
|
||||
"""Recommended to use func instead; Name of the frozen executable file for this component"""
|
||||
icon: str # just the name, no suffix
|
||||
"""Lookup ID for the icon path in LauncherComponents.icon_paths"""
|
||||
cli: bool
|
||||
"""Bool to control if the component gets launched in an appropriate Terminal for the OS"""
|
||||
func: Optional[Callable]
|
||||
"""
|
||||
Function that gets called when the component gets launched
|
||||
Any arg besides the component name arg is passed into the func as well, so handling *args is suggested
|
||||
"""
|
||||
file_identifier: Optional[Callable[[str], bool]]
|
||||
"""
|
||||
Function that is run against patch file arg to identify which component is appropriate to launch
|
||||
If the function is an Instance of SuffixIdentifier the suffixes will also be valid for the Open Patch component
|
||||
"""
|
||||
game_name: Optional[str]
|
||||
"""Game name to identify component when handling launch links from WebHost"""
|
||||
supports_uri: Optional[bool]
|
||||
"""Bool to identify if a component supports being launched by launch links from WebHost"""
|
||||
|
||||
def __init__(self, display_name: str, script_name: Optional[str] = None, frozen_name: Optional[str] = None,
|
||||
cli: bool = False, icon: str = 'icon', component_type: Optional[Type] = None,
|
||||
|
||||
@@ -10,7 +10,7 @@ import base64
|
||||
import enum
|
||||
import json
|
||||
import sys
|
||||
import typing
|
||||
from typing import Any, Sequence
|
||||
|
||||
|
||||
BIZHAWK_SOCKET_PORT_RANGE_START = 43055
|
||||
@@ -44,10 +44,10 @@ class SyncError(Exception):
|
||||
|
||||
|
||||
class BizHawkContext:
|
||||
streams: typing.Optional[typing.Tuple[asyncio.StreamReader, asyncio.StreamWriter]]
|
||||
streams: tuple[asyncio.StreamReader, asyncio.StreamWriter] | None
|
||||
connection_status: ConnectionStatus
|
||||
_lock: asyncio.Lock
|
||||
_port: typing.Optional[int]
|
||||
_port: int | None
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.streams = None
|
||||
@@ -122,12 +122,12 @@ async def get_script_version(ctx: BizHawkContext) -> int:
|
||||
return int(await ctx._send_message("VERSION"))
|
||||
|
||||
|
||||
async def send_requests(ctx: BizHawkContext, req_list: typing.List[typing.Dict[str, typing.Any]]) -> typing.List[typing.Dict[str, typing.Any]]:
|
||||
async def send_requests(ctx: BizHawkContext, req_list: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||
"""Sends a list of requests to the BizHawk connector and returns their responses.
|
||||
|
||||
It's likely you want to use the wrapper functions instead of this."""
|
||||
responses = json.loads(await ctx._send_message(json.dumps(req_list)))
|
||||
errors: typing.List[ConnectorError] = []
|
||||
errors: list[ConnectorError] = []
|
||||
|
||||
for response in responses:
|
||||
if response["type"] == "ERROR":
|
||||
@@ -151,7 +151,7 @@ async def ping(ctx: BizHawkContext) -> None:
|
||||
|
||||
|
||||
async def get_hash(ctx: BizHawkContext) -> str:
|
||||
"""Gets the system name for the currently loaded ROM"""
|
||||
"""Gets the hash value of the currently loaded ROM"""
|
||||
res = (await send_requests(ctx, [{"type": "HASH"}]))[0]
|
||||
|
||||
if res["type"] != "HASH_RESPONSE":
|
||||
@@ -160,6 +160,16 @@ async def get_hash(ctx: BizHawkContext) -> str:
|
||||
return res["value"]
|
||||
|
||||
|
||||
async def get_memory_size(ctx: BizHawkContext, domain: str) -> int:
|
||||
"""Gets the size in bytes of the specified memory domain"""
|
||||
res = (await send_requests(ctx, [{"type": "MEMORY_SIZE", "domain": domain}]))[0]
|
||||
|
||||
if res["type"] != "MEMORY_SIZE_RESPONSE":
|
||||
raise SyncError(f"Expected response of type MEMORY_SIZE_RESPONSE but got {res['type']}")
|
||||
|
||||
return res["value"]
|
||||
|
||||
|
||||
async def get_system(ctx: BizHawkContext) -> str:
|
||||
"""Gets the system name for the currently loaded ROM"""
|
||||
res = (await send_requests(ctx, [{"type": "SYSTEM"}]))[0]
|
||||
@@ -170,7 +180,7 @@ async def get_system(ctx: BizHawkContext) -> str:
|
||||
return res["value"]
|
||||
|
||||
|
||||
async def get_cores(ctx: BizHawkContext) -> typing.Dict[str, str]:
|
||||
async def get_cores(ctx: BizHawkContext) -> dict[str, str]:
|
||||
"""Gets the preferred cores for systems with multiple cores. Only systems with multiple available cores have
|
||||
entries."""
|
||||
res = (await send_requests(ctx, [{"type": "PREFERRED_CORES"}]))[0]
|
||||
@@ -223,8 +233,8 @@ async def set_message_interval(ctx: BizHawkContext, value: float) -> None:
|
||||
raise SyncError(f"Expected response of type SET_MESSAGE_INTERVAL_RESPONSE but got {res['type']}")
|
||||
|
||||
|
||||
async def guarded_read(ctx: BizHawkContext, read_list: typing.Sequence[typing.Tuple[int, int, str]],
|
||||
guard_list: typing.Sequence[typing.Tuple[int, typing.Sequence[int], str]]) -> typing.Optional[typing.List[bytes]]:
|
||||
async def guarded_read(ctx: BizHawkContext, read_list: Sequence[tuple[int, int, str]],
|
||||
guard_list: Sequence[tuple[int, Sequence[int], str]]) -> list[bytes] | None:
|
||||
"""Reads an array of bytes at 1 or more addresses if and only if every byte in guard_list matches its expected
|
||||
value.
|
||||
|
||||
@@ -252,7 +262,7 @@ async def guarded_read(ctx: BizHawkContext, read_list: typing.Sequence[typing.Tu
|
||||
"domain": domain
|
||||
} for address, size, domain in read_list])
|
||||
|
||||
ret: typing.List[bytes] = []
|
||||
ret: list[bytes] = []
|
||||
for item in res:
|
||||
if item["type"] == "GUARD_RESPONSE":
|
||||
if not item["value"]:
|
||||
@@ -266,7 +276,7 @@ async def guarded_read(ctx: BizHawkContext, read_list: typing.Sequence[typing.Tu
|
||||
return ret
|
||||
|
||||
|
||||
async def read(ctx: BizHawkContext, read_list: typing.Sequence[typing.Tuple[int, int, str]]) -> typing.List[bytes]:
|
||||
async def read(ctx: BizHawkContext, read_list: Sequence[tuple[int, int, str]]) -> list[bytes]:
|
||||
"""Reads data at 1 or more addresses.
|
||||
|
||||
Items in `read_list` should be organized `(address, size, domain)` where
|
||||
@@ -278,8 +288,8 @@ async def read(ctx: BizHawkContext, read_list: typing.Sequence[typing.Tuple[int,
|
||||
return await guarded_read(ctx, read_list, [])
|
||||
|
||||
|
||||
async def guarded_write(ctx: BizHawkContext, write_list: typing.Sequence[typing.Tuple[int, typing.Sequence[int], str]],
|
||||
guard_list: typing.Sequence[typing.Tuple[int, typing.Sequence[int], str]]) -> bool:
|
||||
async def guarded_write(ctx: BizHawkContext, write_list: Sequence[tuple[int, Sequence[int], str]],
|
||||
guard_list: Sequence[tuple[int, Sequence[int], str]]) -> bool:
|
||||
"""Writes data to 1 or more addresses if and only if every byte in guard_list matches its expected value.
|
||||
|
||||
Items in `write_list` should be organized `(address, value, domain)` where
|
||||
@@ -316,7 +326,7 @@ async def guarded_write(ctx: BizHawkContext, write_list: typing.Sequence[typing.
|
||||
return True
|
||||
|
||||
|
||||
async def write(ctx: BizHawkContext, write_list: typing.Sequence[typing.Tuple[int, typing.Sequence[int], str]]) -> None:
|
||||
async def write(ctx: BizHawkContext, write_list: Sequence[tuple[int, Sequence[int], str]]) -> None:
|
||||
"""Writes data to 1 or more addresses.
|
||||
|
||||
Items in write_list should be organized `(address, value, domain)` where
|
||||
|
||||
@@ -5,7 +5,7 @@ A module containing the BizHawkClient base class and metaclass
|
||||
from __future__ import annotations
|
||||
|
||||
import abc
|
||||
from typing import TYPE_CHECKING, Any, ClassVar, Dict, Optional, Tuple, Union
|
||||
from typing import TYPE_CHECKING, Any, ClassVar
|
||||
|
||||
from worlds.LauncherComponents import Component, SuffixIdentifier, Type, components, launch_subprocess
|
||||
|
||||
@@ -24,9 +24,9 @@ components.append(component)
|
||||
|
||||
|
||||
class AutoBizHawkClientRegister(abc.ABCMeta):
|
||||
game_handlers: ClassVar[Dict[Tuple[str, ...], Dict[str, BizHawkClient]]] = {}
|
||||
game_handlers: ClassVar[dict[tuple[str, ...], dict[str, BizHawkClient]]] = {}
|
||||
|
||||
def __new__(cls, name: str, bases: Tuple[type, ...], namespace: Dict[str, Any]) -> AutoBizHawkClientRegister:
|
||||
def __new__(cls, name: str, bases: tuple[type, ...], namespace: dict[str, Any]) -> AutoBizHawkClientRegister:
|
||||
new_class = super().__new__(cls, name, bases, namespace)
|
||||
|
||||
# Register handler
|
||||
@@ -54,7 +54,7 @@ class AutoBizHawkClientRegister(abc.ABCMeta):
|
||||
return new_class
|
||||
|
||||
@staticmethod
|
||||
async def get_handler(ctx: "BizHawkClientContext", system: str) -> Optional[BizHawkClient]:
|
||||
async def get_handler(ctx: "BizHawkClientContext", system: str) -> BizHawkClient | None:
|
||||
for systems, handlers in AutoBizHawkClientRegister.game_handlers.items():
|
||||
if system in systems:
|
||||
for handler in handlers.values():
|
||||
@@ -65,13 +65,13 @@ class AutoBizHawkClientRegister(abc.ABCMeta):
|
||||
|
||||
|
||||
class BizHawkClient(abc.ABC, metaclass=AutoBizHawkClientRegister):
|
||||
system: ClassVar[Union[str, Tuple[str, ...]]]
|
||||
system: ClassVar[str | tuple[str, ...]]
|
||||
"""The system(s) that the game this client is for runs on"""
|
||||
|
||||
game: ClassVar[str]
|
||||
"""The game this client is for"""
|
||||
|
||||
patch_suffix: ClassVar[Optional[Union[str, Tuple[str, ...]]]]
|
||||
patch_suffix: ClassVar[str | tuple[str, ...] | None]
|
||||
"""The file extension(s) this client is meant to open and patch (e.g. ".apz3")"""
|
||||
|
||||
@abc.abstractmethod
|
||||
|
||||
@@ -6,7 +6,7 @@ checking or launching the client, otherwise it will probably cause circular impo
|
||||
import asyncio
|
||||
import enum
|
||||
import subprocess
|
||||
from typing import Any, Dict, Optional
|
||||
from typing import Any
|
||||
|
||||
from CommonClient import CommonContext, ClientCommandProcessor, get_base_parser, server_loop, logger, gui_enabled
|
||||
import Patch
|
||||
@@ -43,15 +43,15 @@ class BizHawkClientContext(CommonContext):
|
||||
command_processor = BizHawkClientCommandProcessor
|
||||
auth_status: AuthStatus
|
||||
password_requested: bool
|
||||
client_handler: Optional[BizHawkClient]
|
||||
slot_data: Optional[Dict[str, Any]] = None
|
||||
rom_hash: Optional[str] = None
|
||||
client_handler: BizHawkClient | None
|
||||
slot_data: dict[str, Any] | None = None
|
||||
rom_hash: str | None = None
|
||||
bizhawk_ctx: BizHawkContext
|
||||
|
||||
watcher_timeout: float
|
||||
"""The maximum amount of time the game watcher loop will wait for an update from the server before executing"""
|
||||
|
||||
def __init__(self, server_address: Optional[str], password: Optional[str]):
|
||||
def __init__(self, server_address: str | None, password: str | None):
|
||||
super().__init__(server_address, password)
|
||||
self.auth_status = AuthStatus.NOT_AUTHENTICATED
|
||||
self.password_requested = False
|
||||
@@ -231,20 +231,27 @@ async def _run_game(rom: str):
|
||||
)
|
||||
|
||||
|
||||
async def _patch_and_run_game(patch_file: str):
|
||||
def _patch_and_run_game(patch_file: str):
|
||||
try:
|
||||
metadata, output_file = Patch.create_rom_file(patch_file)
|
||||
Utils.async_start(_run_game(output_file))
|
||||
return metadata
|
||||
except Exception as exc:
|
||||
logger.exception(exc)
|
||||
return {}
|
||||
|
||||
|
||||
def launch(*launch_args) -> None:
|
||||
def launch(*launch_args: str) -> None:
|
||||
async def main():
|
||||
parser = get_base_parser()
|
||||
parser.add_argument("patch_file", default="", type=str, nargs="?", help="Path to an Archipelago patch file")
|
||||
args = parser.parse_args(launch_args)
|
||||
|
||||
if args.patch_file != "":
|
||||
metadata = _patch_and_run_game(args.patch_file)
|
||||
if "server" in metadata:
|
||||
args.connect = metadata["server"]
|
||||
|
||||
ctx = BizHawkClientContext(args.connect, args.password)
|
||||
ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop")
|
||||
|
||||
@@ -252,9 +259,6 @@ def launch(*launch_args) -> None:
|
||||
ctx.run_gui()
|
||||
ctx.run_cli()
|
||||
|
||||
if args.patch_file != "":
|
||||
Utils.async_start(_patch_and_run_game(args.patch_file))
|
||||
|
||||
watcher_task = asyncio.create_task(_game_watcher(ctx), name="GameWatcher")
|
||||
|
||||
try:
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Dict
|
||||
|
||||
from dataclasses import dataclass
|
||||
from Options import Choice, Option, DefaultOnToggle, DeathLink, Range, Toggle, PerGameCommonOptions
|
||||
|
||||
from Options import Choice, DefaultOnToggle, DeathLink, Range, Toggle, PerGameCommonOptions
|
||||
|
||||
|
||||
class FreeincarnateMax(Range):
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from BaseClasses import MultiWorld, Region, Entrance, LocationProgressType
|
||||
from Options import PerGameCommonOptions
|
||||
from .Locations import location_table, LocationData, AdventureLocation, dragon_room_to_region
|
||||
from .Locations import location_table, AdventureLocation, dragon_room_to_region
|
||||
|
||||
|
||||
def connect(world: MultiWorld, player: int, source: str, target: str, rule: callable = lambda state: True,
|
||||
|
||||
@@ -2,15 +2,15 @@ import hashlib
|
||||
import json
|
||||
import os
|
||||
import zipfile
|
||||
from typing import Optional, Any
|
||||
|
||||
import Utils
|
||||
from .Locations import AdventureLocation, LocationData
|
||||
from settings import get_settings
|
||||
from worlds.Files import APPatch, AutoPatchRegister
|
||||
from typing import Any
|
||||
|
||||
import bsdiff4
|
||||
|
||||
import Utils
|
||||
from settings import get_settings
|
||||
from worlds.Files import APPatch, AutoPatchRegister
|
||||
from .Locations import LocationData
|
||||
|
||||
ADVENTUREHASH: str = "157bddb7192754a45372be196797f284"
|
||||
|
||||
|
||||
|
||||
@@ -1,35 +1,24 @@
|
||||
import base64
|
||||
import copy
|
||||
import itertools
|
||||
import math
|
||||
import os
|
||||
import settings
|
||||
import typing
|
||||
from enum import IntFlag
|
||||
from typing import Any, ClassVar, Dict, List, Optional, Set, Tuple
|
||||
from typing import ClassVar, Dict, Optional, Tuple
|
||||
|
||||
from BaseClasses import Entrance, Item, ItemClassification, MultiWorld, Region, Tutorial, \
|
||||
LocationProgressType
|
||||
import settings
|
||||
from BaseClasses import Item, ItemClassification, MultiWorld, Tutorial, LocationProgressType
|
||||
from Utils import __version__
|
||||
from Options import AssembleOptions
|
||||
from worlds.AutoWorld import WebWorld, World
|
||||
from Fill import fill_restrictive
|
||||
from worlds.generic.Rules import add_rule, set_rule
|
||||
from .Options import DragonRandoType, DifficultySwitchA, DifficultySwitchB, \
|
||||
AdventureOptions
|
||||
from .Rom import get_base_rom_bytes, get_base_rom_path, AdventureDeltaPatch, apply_basepatch, \
|
||||
AdventureAutoCollectLocation
|
||||
from worlds.LauncherComponents import Component, components, SuffixIdentifier
|
||||
from .Items import item_table, ItemData, nothing_item_id, event_table, AdventureItem, standard_item_max
|
||||
from .Locations import location_table, base_location_id, LocationData, get_random_room_in_regions
|
||||
from .Offsets import static_item_data_location, items_ram_start, static_item_element_size, item_position_table, \
|
||||
static_first_dragon_index, connector_port_offset, yorgle_speed_data_location, grundle_speed_data_location, \
|
||||
rhindle_speed_data_location, item_ram_addresses, start_castle_values, start_castle_offset
|
||||
from .Options import DragonRandoType, DifficultySwitchA, DifficultySwitchB, AdventureOptions
|
||||
from .Regions import create_regions
|
||||
from .Rom import get_base_rom_bytes, get_base_rom_path, AdventureDeltaPatch, apply_basepatch, AdventureAutoCollectLocation
|
||||
from .Rules import set_rules
|
||||
|
||||
|
||||
from worlds.LauncherComponents import Component, components, SuffixIdentifier
|
||||
|
||||
# Adventure
|
||||
components.append(Component('Adventure Client', 'AdventureClient', file_identifier=SuffixIdentifier('.apadvn')))
|
||||
|
||||
|
||||
@@ -141,9 +141,12 @@ def set_dw_rules(world: "HatInTimeWorld"):
|
||||
add_dw_rules(world, all_clear)
|
||||
add_rule(main_stamp, main_objective.access_rule)
|
||||
add_rule(all_clear, main_objective.access_rule)
|
||||
# Only set bonus stamp rules if we don't auto complete bonuses
|
||||
# Only set bonus stamp rules to require All Clear if we don't auto complete bonuses
|
||||
if not world.options.DWAutoCompleteBonuses and not world.is_bonus_excluded(all_clear.name):
|
||||
add_rule(bonus_stamps, all_clear.access_rule)
|
||||
else:
|
||||
# As soon as the Main Objective is completed, the bonuses auto-complete.
|
||||
add_rule(bonus_stamps, main_objective.access_rule)
|
||||
|
||||
if world.options.DWShuffle:
|
||||
for i in range(len(world.dw_shuffle)-1):
|
||||
@@ -343,6 +346,7 @@ def create_enemy_events(world: "HatInTimeWorld"):
|
||||
|
||||
def set_enemy_rules(world: "HatInTimeWorld"):
|
||||
no_tourist = "Camera Tourist" in world.excluded_dws or "Camera Tourist" in world.excluded_bonuses
|
||||
difficulty = get_difficulty(world)
|
||||
|
||||
for enemy, regions in hit_list.items():
|
||||
if no_tourist and enemy in bosses:
|
||||
@@ -372,6 +376,14 @@ def set_enemy_rules(world: "HatInTimeWorld"):
|
||||
or state.has("Zipline Unlock - The Lava Cake Path", world.player)
|
||||
or state.has("Zipline Unlock - The Windmill Path", world.player))
|
||||
|
||||
elif enemy == "Toilet":
|
||||
if area == "Toilet of Doom":
|
||||
# The boss firewall is in the way and can only be skipped on Expert logic using a cherry hover.
|
||||
add_rule(event, lambda state: has_paintings(state, world, 1, allow_skip=difficulty == Difficulty.EXPERT))
|
||||
if difficulty < Difficulty.HARD:
|
||||
# Hard logic and above can cross the boss arena gap with a cherry bridge.
|
||||
add_rule(event, lambda state: can_use_hookshot(state, world))
|
||||
|
||||
elif enemy == "Director":
|
||||
if area == "Dead Bird Studio Basement":
|
||||
add_rule(event, lambda state: can_use_hookshot(state, world))
|
||||
@@ -430,7 +442,7 @@ hit_list = {
|
||||
# Bosses
|
||||
"Mafia Boss": ["Down with the Mafia!", "Encore! Encore!", "Boss Rush"],
|
||||
|
||||
"Conductor": ["Dead Bird Studio Basement", "Killing Two Birds", "Boss Rush"],
|
||||
"Director": ["Dead Bird Studio Basement", "Killing Two Birds", "Boss Rush"],
|
||||
"Toilet": ["Toilet of Doom", "Boss Rush"],
|
||||
|
||||
"Snatcher": ["Your Contract has Expired", "Breaching the Contract", "Boss Rush",
|
||||
@@ -454,7 +466,7 @@ triple_enemy_locations = [
|
||||
|
||||
bosses = [
|
||||
"Mafia Boss",
|
||||
"Conductor",
|
||||
"Director",
|
||||
"Toilet",
|
||||
"Snatcher",
|
||||
"Toxic Flower",
|
||||
|
||||
@@ -264,7 +264,6 @@ ahit_locations = {
|
||||
required_hats=[HatType.DWELLER], paintings=3),
|
||||
|
||||
"Subcon Forest - Tall Tree Hookshot Swing": LocData(2000324766, "Subcon Forest Area",
|
||||
required_hats=[HatType.DWELLER],
|
||||
hookshot=True,
|
||||
paintings=3),
|
||||
|
||||
@@ -323,7 +322,7 @@ ahit_locations = {
|
||||
"Alpine Skyline - The Twilight Path": LocData(2000334434, "Alpine Skyline Area", required_hats=[HatType.DWELLER]),
|
||||
"Alpine Skyline - The Twilight Bell: Wide Purple Platform": LocData(2000336478, "The Twilight Bell"),
|
||||
"Alpine Skyline - The Twilight Bell: Ice Platform": LocData(2000335826, "The Twilight Bell"),
|
||||
"Alpine Skyline - Goat Outpost Horn": LocData(2000334760, "Alpine Skyline Area"),
|
||||
"Alpine Skyline - Goat Outpost Horn": LocData(2000334760, "Alpine Skyline Area (TIHS)", hookshot=True),
|
||||
"Alpine Skyline - Windy Passage": LocData(2000334776, "Alpine Skyline Area (TIHS)", hookshot=True),
|
||||
"Alpine Skyline - The Windmill: Inside Pon Cluster": LocData(2000336395, "The Windmill"),
|
||||
"Alpine Skyline - The Windmill: Entrance": LocData(2000335783, "The Windmill"),
|
||||
@@ -407,7 +406,7 @@ act_completions = {
|
||||
hit_type=HitType.umbrella_or_brewing, hookshot=True, paintings=1),
|
||||
|
||||
"Act Completion (Queen Vanessa's Manor)": LocData(2000312017, "Queen Vanessa's Manor",
|
||||
hit_type=HitType.umbrella, paintings=1),
|
||||
hit_type=HitType.dweller_bell, paintings=1),
|
||||
|
||||
"Act Completion (Mail Delivery Service)": LocData(2000312032, "Mail Delivery Service",
|
||||
required_hats=[HatType.SPRINT]),
|
||||
@@ -878,7 +877,7 @@ snatcher_coins = {
|
||||
dlc_flags=HatDLC.death_wish),
|
||||
|
||||
"Snatcher Coin - Top of HQ (DW: BTH)": LocData(0, "Beat the Heat", snatcher_coin="Snatcher Coin - Top of HQ",
|
||||
dlc_flags=HatDLC.death_wish),
|
||||
hit_type=HitType.umbrella, dlc_flags=HatDLC.death_wish),
|
||||
|
||||
"Snatcher Coin - Top of Tower": LocData(0, "Mafia Town Area (HUMT)", snatcher_coin="Snatcher Coin - Top of Tower",
|
||||
dlc_flags=HatDLC.death_wish),
|
||||
|
||||
@@ -414,7 +414,7 @@ def set_moderate_rules(world: "HatInTimeWorld"):
|
||||
|
||||
# Moderate: Mystifying Time Mesa time trial without hats
|
||||
set_rule(world.multiworld.get_location("Alpine Skyline - Mystifying Time Mesa: Zipline", world.player),
|
||||
lambda state: can_use_hookshot(state, world))
|
||||
lambda state: True)
|
||||
|
||||
# Moderate: Goat Refinery from TIHS with Sprint only
|
||||
add_rule(world.multiworld.get_location("Alpine Skyline - Goat Refinery", world.player),
|
||||
@@ -493,9 +493,6 @@ def set_hard_rules(world: "HatInTimeWorld"):
|
||||
lambda state: has_paintings(state, world, 3, True))
|
||||
|
||||
# SDJ
|
||||
add_rule(world.multiworld.get_location("Subcon Forest - Long Tree Climb Chest", world.player),
|
||||
lambda state: can_use_hat(state, world, HatType.SPRINT) and has_paintings(state, world, 2), "or")
|
||||
|
||||
add_rule(world.multiworld.get_location("Act Completion (Time Rift - Curly Tail Trail)", world.player),
|
||||
lambda state: can_use_hat(state, world, HatType.SPRINT), "or")
|
||||
|
||||
@@ -533,7 +530,10 @@ def set_expert_rules(world: "HatInTimeWorld"):
|
||||
# Expert: Mafia Town - Above Boats, Top of Lighthouse, and Hot Air Balloon with nothing
|
||||
set_rule(world.multiworld.get_location("Mafia Town - Above Boats", world.player), lambda state: True)
|
||||
set_rule(world.multiworld.get_location("Mafia Town - Top of Lighthouse", world.player), lambda state: True)
|
||||
set_rule(world.multiworld.get_location("Mafia Town - Hot Air Balloon", world.player), lambda state: True)
|
||||
# There are not enough buckets/beach balls to bucket/ball hover in Heating Up Mafia Town, so any other Mafia Town
|
||||
# act is required.
|
||||
add_rule(world.multiworld.get_location("Mafia Town - Hot Air Balloon", world.player),
|
||||
lambda state: state.can_reach_region("Mafia Town Area", world.player), "or")
|
||||
|
||||
# Expert: Clear Dead Bird Studio with nothing
|
||||
for loc in world.multiworld.get_region("Dead Bird Studio - Post Elevator Area", world.player).locations:
|
||||
@@ -590,7 +590,7 @@ def set_expert_rules(world: "HatInTimeWorld"):
|
||||
|
||||
if world.is_dlc2():
|
||||
# Expert: clear Rush Hour with nothing
|
||||
if not world.options.NoTicketSkips:
|
||||
if world.options.NoTicketSkips != NoTicketSkips.option_true:
|
||||
set_rule(world.multiworld.get_location("Act Completion (Rush Hour)", world.player), lambda state: True)
|
||||
else:
|
||||
set_rule(world.multiworld.get_location("Act Completion (Rush Hour)", world.player),
|
||||
@@ -739,7 +739,7 @@ def set_dlc1_rules(world: "HatInTimeWorld"):
|
||||
|
||||
# This particular item isn't present in Act 3 for some reason, yes in vanilla too
|
||||
add_rule(world.multiworld.get_location("The Arctic Cruise - Toilet", world.player),
|
||||
lambda state: state.can_reach("Bon Voyage!", "Region", world.player)
|
||||
lambda state: (state.can_reach("Bon Voyage!", "Region", world.player) and can_use_hookshot(state, world))
|
||||
or state.can_reach("Ship Shape", "Region", world.player))
|
||||
|
||||
|
||||
|
||||
@@ -119,7 +119,9 @@ def KholdstareDefeatRule(state, player: int) -> bool:
|
||||
|
||||
|
||||
def VitreousDefeatRule(state, player: int) -> bool:
|
||||
return can_shoot_arrows(state, player) or has_melee_weapon(state, player)
|
||||
return ((can_shoot_arrows(state, player) and can_use_bombs(state, player, 10))
|
||||
or can_shoot_arrows(state, player, 35) or state.has("Silver Bow", player)
|
||||
or has_melee_weapon(state, player))
|
||||
|
||||
|
||||
def TrinexxDefeatRule(state, player: int) -> bool:
|
||||
|
||||
@@ -464,7 +464,7 @@ async def track_locations(ctx, roomid, roomdata) -> bool:
|
||||
snes_logger.info(f"Discarding recent {len(new_locations)} checks as ROM Status has changed.")
|
||||
return False
|
||||
else:
|
||||
await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": new_locations}])
|
||||
await ctx.check_locations(new_locations)
|
||||
await snes_flush_writes(ctx)
|
||||
return True
|
||||
|
||||
|
||||
@@ -484,8 +484,7 @@ def generate_itempool(world):
|
||||
if multiworld.randomize_cost_types[player]:
|
||||
# Heart and Arrow costs require all Heart Container/Pieces and Arrow Upgrades to be advancement items for logic
|
||||
for item in items:
|
||||
if (item.name in ("Boss Heart Container", "Sanctuary Heart Container", "Piece of Heart")
|
||||
or "Arrow Upgrade" in item.name):
|
||||
if item.name in ("Boss Heart Container", "Sanctuary Heart Container", "Piece of Heart"):
|
||||
item.classification = ItemClassification.progression
|
||||
else:
|
||||
# Otherwise, logic has some branches where having 4 hearts is one possible requirement (of several alternatives)
|
||||
@@ -713,7 +712,7 @@ def get_pool_core(world, player: int):
|
||||
pool.remove("Rupees (20)")
|
||||
|
||||
if retro_bow:
|
||||
replace = {'Single Arrow', 'Arrows (10)', 'Arrow Upgrade (+5)', 'Arrow Upgrade (+10)', 'Arrow Upgrade (50)'}
|
||||
replace = {'Single Arrow', 'Arrows (10)', 'Arrow Upgrade (+5)', 'Arrow Upgrade (+10)', 'Arrow Upgrade (70)'}
|
||||
pool = ['Rupees (5)' if item in replace else item for item in pool]
|
||||
if world.small_key_shuffle[player] == small_key_shuffle.option_universal:
|
||||
pool.extend(diff.universal_keys)
|
||||
|
||||
@@ -7,7 +7,7 @@ from worlds.AutoWorld import World
|
||||
def GetBeemizerItem(world, player: int, item):
|
||||
item_name = item if isinstance(item, str) else item.name
|
||||
|
||||
if item_name not in trap_replaceable:
|
||||
if item_name not in trap_replaceable or player in world.groups:
|
||||
return item
|
||||
|
||||
# first roll - replaceable item should be replaced, within beemizer_total_chance
|
||||
@@ -110,9 +110,9 @@ item_table = {'Bow': ItemData(IC.progression, None, 0x0B, 'You have\nchosen the\
|
||||
'Crystal 7': ItemData(IC.progression, 'Crystal', (0x08, 0x34, 0x64, 0x40, 0x7C, 0x06), None, None, None, None, None, None, "a blue crystal"),
|
||||
'Single Arrow': ItemData(IC.filler, None, 0x43, 'a lonely arrow\nsits here.', 'and the arrow', 'stick-collecting kid', 'sewing needle for sale', 'fungus for arrow', 'archer boy sews again', 'an arrow'),
|
||||
'Arrows (10)': ItemData(IC.filler, None, 0x44, 'This will give\nyou ten shots\nwith your bow!', 'and the arrow pack','stick-collecting kid', 'sewing kit for sale', 'fungus for arrows', 'archer boy sews again','ten arrows'),
|
||||
'Arrow Upgrade (+10)': ItemData(IC.useful, None, 0x54, 'increase arrow\nstorage, low\nlow price', 'and the quiver', 'quiver-enlarging kid', 'arrow boost for sale', 'witch and more skewers', 'upgrade boy sews more again', 'arrow capacity'),
|
||||
'Arrow Upgrade (+5)': ItemData(IC.useful, None, 0x53, 'increase arrow\nstorage, low\nlow price', 'and the quiver', 'quiver-enlarging kid', 'arrow boost for sale', 'witch and more skewers', 'upgrade boy sews more again', 'arrow capacity'),
|
||||
'Arrow Upgrade (70)': ItemData(IC.useful, None, 0x4D, 'increase arrow\nstorage, low\nlow price', 'and the quiver', 'quiver-enlarging kid', 'arrow boost for sale', 'witch and more skewers', 'upgrade boy sews more again', 'arrow capacity'),
|
||||
'Arrow Upgrade (+10)': ItemData(IC.progression_skip_balancing, None, 0x54, 'increase arrow\nstorage, low\nlow price', 'and the quiver', 'quiver-enlarging kid', 'arrow boost for sale', 'witch and more skewers', 'upgrade boy sews more again', 'arrow capacity'),
|
||||
'Arrow Upgrade (+5)': ItemData(IC.progression_skip_balancing, None, 0x53, 'increase arrow\nstorage, low\nlow price', 'and the quiver', 'quiver-enlarging kid', 'arrow boost for sale', 'witch and more skewers', 'upgrade boy sews more again', 'arrow capacity'),
|
||||
'Arrow Upgrade (70)': ItemData(IC.progression_skip_balancing, None, 0x4D, 'increase arrow\nstorage, low\nlow price', 'and the quiver', 'quiver-enlarging kid', 'arrow boost for sale', 'witch and more skewers', 'upgrade boy sews more again', 'arrow capacity'),
|
||||
'Single Bomb': ItemData(IC.filler, None, 0x27, 'I make things\ngo BOOM! But\njust once.', 'and the explosion', 'the bomb-holding kid', 'firecracker for sale', 'blend fungus into bomb', '\'splosion boy explodes again', 'a bomb'),
|
||||
'Bombs (3)': ItemData(IC.filler, None, 0x28, 'I make things\ngo triple\nBOOM!!!', 'and the explosions', 'the bomb-holding kid', 'firecrackers for sale', 'blend fungus into bombs', '\'splosion boy explodes again', 'three bombs'),
|
||||
'Bombs (10)': ItemData(IC.filler, None, 0x31, 'I make things\ngo BOOM! Ten\ntimes!', 'and the explosions', 'the bomb-holding kid', 'firecrackers for sale', 'blend fungus into bombs', '\'splosion boy explodes again', 'ten bombs'),
|
||||
|
||||
@@ -592,9 +592,9 @@ def global_rules(multiworld: MultiWorld, player: int):
|
||||
lambda state: can_kill_most_things(state, player, 8) and has_fire_source(state, player) and state.multiworld.get_entrance('Ganons Tower Torch Rooms', player).parent_region.dungeon.bosses['middle'].can_defeat(state))
|
||||
set_rule(multiworld.get_location('Ganons Tower - Mini Helmasaur Key Drop', player), lambda state: can_kill_most_things(state, player, 1))
|
||||
set_rule(multiworld.get_location('Ganons Tower - Pre-Moldorm Chest', player),
|
||||
lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 7))
|
||||
lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 7) and can_use_bombs(state, player))
|
||||
set_rule(multiworld.get_entrance('Ganons Tower Moldorm Door', player),
|
||||
lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 8))
|
||||
lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 8) and can_use_bombs(state, player))
|
||||
set_rule(multiworld.get_entrance('Ganons Tower Moldorm Gap', player),
|
||||
lambda state: state.has('Hookshot', player) and state.multiworld.get_entrance('Ganons Tower Moldorm Gap', player).parent_region.dungeon.bosses['top'].can_defeat(state))
|
||||
set_defeat_dungeon_boss_rule(multiworld.get_location('Agahnim 2', player))
|
||||
|
||||
@@ -170,7 +170,8 @@ def push_shop_inventories(multiworld):
|
||||
# Retro Bow arrows will already have been pushed
|
||||
if (not multiworld.retro_bow[location.player]) or ((item_name, location.item.player)
|
||||
!= ("Single Arrow", location.player)):
|
||||
location.shop.push_inventory(location.shop_slot, item_name, location.shop_price,
|
||||
location.shop.push_inventory(location.shop_slot, item_name,
|
||||
round(location.shop_price * get_price_modifier(location.item)),
|
||||
1, location.item.player if location.item.player != location.player else 0,
|
||||
location.shop_price_type)
|
||||
location.shop_price = location.shop.inventory[location.shop_slot]["price"] = min(location.shop_price,
|
||||
|
||||
@@ -15,18 +15,18 @@ def can_bomb_clip(state: CollectionState, region: LTTPRegion, player: int) -> bo
|
||||
|
||||
def can_buy_unlimited(state: CollectionState, item: str, player: int) -> bool:
|
||||
return any(shop.region.player == player and shop.has_unlimited(item) and shop.region.can_reach(state) for
|
||||
shop in state.multiworld.shops)
|
||||
shop in state.multiworld.shops)
|
||||
|
||||
|
||||
def can_buy(state: CollectionState, item: str, player: int) -> bool:
|
||||
return any(shop.region.player == player and shop.has(item) and shop.region.can_reach(state) for
|
||||
shop in state.multiworld.shops)
|
||||
shop in state.multiworld.shops)
|
||||
|
||||
|
||||
def can_shoot_arrows(state: CollectionState, player: int) -> bool:
|
||||
def can_shoot_arrows(state: CollectionState, player: int, count: int = 0) -> bool:
|
||||
if state.multiworld.retro_bow[player]:
|
||||
return (state.has('Bow', player) or state.has('Silver Bow', player)) and can_buy(state, 'Single Arrow', player)
|
||||
return state.has('Bow', player) or state.has('Silver Bow', player)
|
||||
return (state.has('Bow', player) or state.has('Silver Bow', player)) and can_hold_arrows(state, player, count)
|
||||
|
||||
|
||||
def has_triforce_pieces(state: CollectionState, player: int) -> bool:
|
||||
@@ -61,13 +61,13 @@ def heart_count(state: CollectionState, player: int) -> int:
|
||||
# Warning: This only considers items that are marked as advancement items
|
||||
diff = state.multiworld.worlds[player].difficulty_requirements
|
||||
return min(state.count('Boss Heart Container', player), diff.boss_heart_container_limit) \
|
||||
+ state.count('Sanctuary Heart Container', player) \
|
||||
+ state.count('Sanctuary Heart Container', player) \
|
||||
+ min(state.count('Piece of Heart', player), diff.heart_piece_limit) // 4 \
|
||||
+ 3 # starting hearts
|
||||
+ 3 # starting hearts
|
||||
|
||||
|
||||
def can_extend_magic(state: CollectionState, player: int, smallmagic: int = 16,
|
||||
fullrefill: bool = False): # This reflects the total magic Link has, not the total extra he has.
|
||||
fullrefill: bool = False): # This reflects the total magic Link has, not the total extra he has.
|
||||
basemagic = 8
|
||||
if state.has('Magic Upgrade (1/4)', player):
|
||||
basemagic = 32
|
||||
@@ -84,11 +84,18 @@ def can_extend_magic(state: CollectionState, player: int, smallmagic: int = 16,
|
||||
|
||||
|
||||
def can_hold_arrows(state: CollectionState, player: int, quantity: int):
|
||||
arrows = 30 + ((state.count("Arrow Upgrade (+5)", player) * 5) + (state.count("Arrow Upgrade (+10)", player) * 10)
|
||||
+ (state.count("Bomb Upgrade (50)", player) * 50))
|
||||
# Arrow Upgrade (+5) beyond the 6th gives +10
|
||||
arrows += max(0, ((state.count("Arrow Upgrade (+5)", player) - 6) * 10))
|
||||
return min(70, arrows) >= quantity
|
||||
if state.multiworld.worlds[player].options.shuffle_capacity_upgrades:
|
||||
if quantity == 0:
|
||||
return True
|
||||
if state.has("Arrow Upgrade (70)", player):
|
||||
arrows = 70
|
||||
else:
|
||||
arrows = (30 + (state.count("Arrow Upgrade (+5)", player) * 5)
|
||||
+ (state.count("Arrow Upgrade (+10)", player) * 10))
|
||||
# Arrow Upgrade (+5) beyond the 6th gives +10
|
||||
arrows += max(0, ((state.count("Arrow Upgrade (+5)", player) - 6) * 10))
|
||||
return min(70, arrows) >= quantity
|
||||
return quantity <= 30 or state.has("Capacity Upgrade Shop", player)
|
||||
|
||||
|
||||
def can_use_bombs(state: CollectionState, player: int, quantity: int = 1) -> bool:
|
||||
@@ -146,19 +153,19 @@ def can_get_good_bee(state: CollectionState, player: int) -> bool:
|
||||
def can_retrieve_tablet(state: CollectionState, player: int) -> bool:
|
||||
return state.has('Book of Mudora', player) and (has_beam_sword(state, player) or
|
||||
(state.multiworld.swordless[player] and
|
||||
state.has("Hammer", player)))
|
||||
state.has("Hammer", player)))
|
||||
|
||||
|
||||
def has_sword(state: CollectionState, player: int) -> bool:
|
||||
return state.has('Fighter Sword', player) \
|
||||
or state.has('Master Sword', player) \
|
||||
or state.has('Tempered Sword', player) \
|
||||
or state.has('Golden Sword', player)
|
||||
or state.has('Master Sword', player) \
|
||||
or state.has('Tempered Sword', player) \
|
||||
or state.has('Golden Sword', player)
|
||||
|
||||
|
||||
def has_beam_sword(state: CollectionState, player: int) -> bool:
|
||||
return state.has('Master Sword', player) or state.has('Tempered Sword', player) or state.has('Golden Sword',
|
||||
player)
|
||||
player)
|
||||
|
||||
|
||||
def has_melee_weapon(state: CollectionState, player: int) -> bool:
|
||||
@@ -171,9 +178,9 @@ def has_fire_source(state: CollectionState, player: int) -> bool:
|
||||
|
||||
def can_melt_things(state: CollectionState, player: int) -> bool:
|
||||
return state.has('Fire Rod', player) or \
|
||||
(state.has('Bombos', player) and
|
||||
(state.multiworld.swordless[player] or
|
||||
has_sword(state, player)))
|
||||
(state.has('Bombos', player) and
|
||||
(state.multiworld.swordless[player] or
|
||||
has_sword(state, player)))
|
||||
|
||||
|
||||
def has_misery_mire_medallion(state: CollectionState, player: int) -> bool:
|
||||
|
||||
@@ -1,224 +1,123 @@
|
||||
# Guía de instalación para A Link to the Past Randomizer Multiworld
|
||||
|
||||
<div id="tutorial-video-container">
|
||||
<iframe width="560" height="315" src="https://www.youtube-nocookie.com/embed/mJKEHaiyR_Y" frameborder="0"
|
||||
allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen>
|
||||
</iframe>
|
||||
</div>
|
||||
|
||||
## Software requerido
|
||||
|
||||
- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases)
|
||||
- [QUsb2Snes](https://github.com/Skarsnik/QUsb2snes/releases) (Incluido en Multiworld Utilities)
|
||||
- Hardware o software capaz de cargar y ejecutar archivos de ROM de SNES
|
||||
- Un emulador capaz de ejecutar scripts Lua
|
||||
([snes9x rr](https://github.com/gocha/snes9x-rr/releases),
|
||||
- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases).
|
||||
- [SNI](https://github.com/alttpo/sni/releases). Esto está incluido automáticamente en la instalación de Archipelago.
|
||||
- SNI no es compatible con (Q)Usb2Snes.
|
||||
- Hardware o software capaz de cargar y ejecutar archivos de ROM de SNES, por ejemplo:
|
||||
- Un emulador capaz de conectarse a SNI
|
||||
([snes9x-nwa](https://github.com/Skarsnik/snes9x-emunwa/releases), [snes9x-rr](https://github.com/gocha/snes9x-rr/releases),
|
||||
[BSNES-plus](https://github.com/black-sliver/bsnes-plus),
|
||||
[BizHawk](https://tasvideos.org/BizHawk), o
|
||||
[RetroArch](https://retroarch.com?page=platforms) 1.10.1 o más nuevo). O,
|
||||
- Un flashcart SD2SNES, [FXPak Pro](https://krikzz.com/store/home/54-fxpak-pro.html), o otro hardware compatible
|
||||
[RetroArch](https://retroarch.com?page=platforms) 1.10.1 o más nuevo).
|
||||
- Un SD2SNES, [FXPak Pro](https://krikzz.com/store/home/54-fxpak-pro.html), u otro hardware compatible. **nota:
|
||||
Las SNES minis modificadas no tienen soporte de SNI. Algunos usuarios dicen haber tenido éxito con Qusb2Snes para esta consola,
|
||||
pero no tiene soporte.**
|
||||
- Tu archivo ROM japones v1.0, probablemente se llame `Zelda no Densetsu - Kamigami no Triforce (Japan).sfc`
|
||||
|
||||
## Procedimiento de instalación
|
||||
|
||||
### Instalación en Windows
|
||||
|
||||
1. Descarga e instala MultiWorld Utilities desde el enlace anterior, asegurando que instalamos la versión más reciente.
|
||||
**El archivo esta localizado en la sección "assets" en la parte inferior de la información de versión**. Si tu
|
||||
intención es jugar la versión normal de multiworld, necesitarás el archivo `Setup.Archipelago.exe`
|
||||
- Si estas interesado en jugar la variante que aleatoriza las puertas internas de las mazmorras, necesitaras bajar '
|
||||
Setup.BerserkerMultiWorld.Doors.exe'
|
||||
- Durante el proceso de instalación, se te pedirá donde esta situado tu archivo ROM japonés v1.0. Si ya habías
|
||||
instalado este software con anterioridad y simplemente estas actualizando, no se te pedirá la localización del
|
||||
archivo una segunda vez.
|
||||
- Puede ser que el programa pida la instalación de Microsoft Visual C++. Si ya lo tienes en tu ordenador (
|
||||
posiblemente por que un juego de Steam ya lo haya instalado), el instalador no te pedirá su instalación.
|
||||
|
||||
2. Si estas usando un emulador, deberías asignar la versión capaz de ejecutar scripts Lua como programa por defecto para
|
||||
lanzar ficheros de ROM de SNES.
|
||||
1. Extrae tu emulador al escritorio, o cualquier sitio que después recuerdes.
|
||||
2. Haz click derecho en un fichero de ROM (ha de tener la extensión sfc) y selecciona **Abrir con...**
|
||||
3. Marca la opción **Usar siempre esta aplicación para abrir los archivos .sfc**
|
||||
4. Baja hasta el final de la lista y haz click en la opción **Buscar otra aplicación en el equipo** (Si usas Windows
|
||||
10 es posible que debas hacer click en **Más aplicaciones**)
|
||||
5. Busca el archivo .exe de tu emulador y haz click en **Abrir**. Este archivo debe estar en el directorio donde
|
||||
extrajiste en el paso 1.
|
||||
|
||||
### Instalación en Macintosh
|
||||
|
||||
- ¡Necesitamos voluntarios para rellenar esta seccion! Contactad con **Farrak Kilhn** (en inglés) en Discord si queréis
|
||||
ayudar.
|
||||
|
||||
## Configurar tu archivo YAML
|
||||
|
||||
### Que es un archivo YAML y por qué necesito uno?
|
||||
|
||||
Tu archivo YAML contiene un conjunto de opciones de configuración que proveen al generador con información sobre como
|
||||
debe generar tu juego. Cada jugador en una partida de multiworld proveerá su propio fichero YAML. Esta configuración
|
||||
permite que cada jugador disfrute de una experiencia personalizada a su gusto, y cada jugador dentro de la misma partida
|
||||
de multiworld puede tener diferentes opciones.
|
||||
|
||||
### Donde puedo obtener un fichero YAML?
|
||||
|
||||
La página "[Generate Game](/games/A%20Link%20to%20the%20Past/player-options)" en el sitio web te permite configurar tu
|
||||
configuración personal y descargar un fichero "YAML".
|
||||
|
||||
### Configuración YAML avanzada
|
||||
|
||||
Una version mas avanzada del fichero Yaml puede ser creada usando la pagina
|
||||
["Weighted settings"](/games/A Link to the Past/weighted-options),
|
||||
la cual te permite tener almacenadas hasta 3 preajustes. La pagina "Weighted Settings" tiene muchas opciones
|
||||
representadas con controles deslizantes. Esto permite elegir cuan probable los valores de una categoría pueden ser
|
||||
elegidos sobre otros de la misma.
|
||||
|
||||
Por ejemplo, imagina que el generador crea un cubo llamado "map_shuffle", y pone trozos de papel doblado en él por cada
|
||||
sub-opción. Ademas imaginemos que tu valor elegido para "on" es 20 y el elegido para "off" es 40.
|
||||
|
||||
Por tanto, en este ejemplo, habrán 60 trozos de papel. 20 para "on" y 40 para "off". Cuando el generador esta decidiendo
|
||||
si activar o no "map shuffle" para tu partida, meterá la mano en el cubo y sacara un trozo de papel al azar. En este
|
||||
ejemplo, es mucho mas probable (2 de cada 3 veces (40/60)) que "map shuffle" esté desactivado.
|
||||
|
||||
Si quieres que una opción no pueda ser escogida, simplemente asigna el valor 0 a dicha opción. Recuerda que cada opción
|
||||
debe tener al menos un valor mayor que cero, si no la generación fallará.
|
||||
|
||||
### Verificando tu archivo YAML
|
||||
|
||||
Si quieres validar que tu fichero YAML para asegurarte que funciona correctamente, puedes hacerlo en la pagina
|
||||
[YAML Validator](/check).
|
||||
|
||||
## Generar una partida para un jugador
|
||||
|
||||
1. Navega a [la pagina Generate game](/games/A%20Link%20to%20the%20Past/player-options), configura tus opciones, haz
|
||||
click en el boton "Generate game".
|
||||
2. Se te redigirá a una pagina "Seed Info", donde puedes descargar tu archivo de parche.
|
||||
3. Haz doble click en tu fichero de parche, y el emulador debería ejecutar tu juego automáticamente. Como el Cliente no
|
||||
es necesario para partidas de un jugador, puedes cerrarlo junto a la pagina web (que tiene como titulo "Multiworld
|
||||
WebUI") que se ha abierto automáticamente.
|
||||
|
||||
## Unirse a una partida MultiWorld
|
||||
1. Descarga e instala [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases/latest).
|
||||
**El archivo del instalador se encuentra en la sección de assets al final de la información de version**.
|
||||
2. La primera vez que realices una generación local o parchees tu juego, se te pedirá que ubiques tu archivo ROM base.
|
||||
Este es tu archivo ROM de Link to the Past japonés. Esto sólo debe hacerse una vez.
|
||||
|
||||
4. Si estás usando un emulador, deberías de asignar tu emulador con compatibilidad con Lua como el programa por defecto para abrir archivos
|
||||
ROM.
|
||||
1. Extrae la carpeta de tu emulador al Escritorio, o algún otro sitio que vayas a recordar.
|
||||
2. Haz click derecho en un archivo ROM y selecciona **Abrir con...**
|
||||
3. Marca la casilla junto a **Usar siempre este programa para abrir archivos .sfc**
|
||||
4. Baja al final de la lista y haz click en el texto gris **Buscar otro programa en este PC**
|
||||
5. Busca el archivo `.exe` de tu emulador y haz click en **Abrir**. Este archivo debería de encontrarse dentro de la carpeta que
|
||||
extrajiste en el paso uno.
|
||||
|
||||
### Obtener el fichero de parche y crea tu ROM
|
||||
|
||||
Cuando te unes a una partida multiworld, debes proveer tu fichero YAML a quien sea el creador de la partida. Una vez
|
||||
Cuando te unas a una partida multiworld, se te pedirá enviarle tu archivo de configuración a quien quiera que esté creando. Una vez eso
|
||||
este hecho, el creador te devolverá un enlace para descargar el parche o un fichero zip conteniendo todos los ficheros
|
||||
de parche de la partida Tu fichero de parche debe tener la extensión `.aplttp`.
|
||||
de parche de la partida. Tu fichero de parche debe de tener la extensión `.aplttp`.
|
||||
|
||||
Pon tu fichero de parche en el escritorio o en algún sitio conveniente, y haz doble click. Esto debería ejecutar
|
||||
automáticamente el cliente, y ademas creara la rom en el mismo directorio donde este el fichero de parche.
|
||||
Pon tu fichero de parche en el escritorio o en algún sitio conveniente, y hazle doble click. Esto debería ejecutar
|
||||
automáticamente el cliente, y además creará la rom en el mismo directorio donde este el fichero de parche.
|
||||
|
||||
### Conectar al cliente
|
||||
|
||||
#### Con emulador
|
||||
|
||||
Cuando el cliente se lance automáticamente, QUsb2Snes debería haberse ejecutado también. Si es la primera vez que lo
|
||||
ejecutas, puedes ser que el firewall de Windows te pregunte si le permites la comunicación.
|
||||
Cuando el cliente se lance automáticamente, SNI debería de ejecutarse en segundo plano. Si es la
|
||||
primera vez que se ejecuta, tal vez se te pida permitir que se comunique a través del firewall de Windows
|
||||
|
||||
#### snes9x-nwa
|
||||
|
||||
1. Haz click en el menu Network y marca 'Enable Emu Network Control
|
||||
2. Carga tu archivo ROM si no lo habías hecho antes
|
||||
|
||||
##### snes9x-rr
|
||||
|
||||
1. Carga tu fichero de ROM, si no lo has hecho ya
|
||||
1. Carga tu fichero ROM, si no lo has hecho ya
|
||||
2. Abre el menu "File" y situa el raton en **Lua Scripting**
|
||||
3. Haz click en **New Lua Script Window...**
|
||||
4. En la nueva ventana, haz click en **Browse...**
|
||||
5. Navega hacia el directorio donde este situado snes9x-rr, entra en el directorio `lua`, y
|
||||
escoge `multibridge.lua`
|
||||
6. Observa que se ha asignado un nombre al dispositivo, y el cliente muestra "SNES Device: Connected", con el mismo
|
||||
nombre en la esquina superior izquierda.
|
||||
5. Selecciona el archivo lua conector incluido con tu cliente
|
||||
- Busca en la carpeta de Archipelago `/SNI/lua/`.
|
||||
6. Si ves un error mientras carga el script que dice `socket.dll missing` o algo similar, ve a la carpeta de
|
||||
el lua que estas usando en tu gestor de archivos y copia el `socket.dll` a la raíz de tu instalación de snes9x.
|
||||
|
||||
##### BNES-Plus
|
||||
|
||||
1. Cargue su archivo ROM si aún no se ha cargado.
|
||||
2. El emulador debería conectarse automáticamente mientras SNI se está ejecutando.
|
||||
|
||||
##### BizHawk
|
||||
|
||||
1. Asegurate que se ha cargado el nucleo BSNES. Debes hacer esto en el menu Tools y siguiento estas opciones:
|
||||
`Config --> Cores --> SNES --> BSNES`
|
||||
Una vez cambiado el nucleo cargado, BizHawk ha de ser reiniciado.
|
||||
1. Asegurate que se ha cargado el núcleo BSNES. Se hace en la barra de menú principal, bajo:
|
||||
- (≤ 2.8) `Config` 〉 `Cores` 〉 `SNES` 〉 `BSNES`
|
||||
- (≥ 2.9) `Config` 〉 `Preferred Cores` 〉 `SNES` 〉 `BSNESv115+`
|
||||
2. Carga tu fichero de ROM, si no lo has hecho ya.
|
||||
3. Haz click en el menu Tools y en la opción **Lua Console**
|
||||
4. Haz click en el botón para abrir un nuevo script Lua.
|
||||
5. Navega al directorio de instalación de MultiWorld Utilities, y en los siguiente directorios:
|
||||
`QUsb2Snes/Qusb2Snes/LuaBridge`
|
||||
6. Selecciona `luabridge.lua` y haz click en Abrir.
|
||||
7. Observa que se ha asignado un nombre al dispositivo, y el cliente muestra "SNES Device: Connected", con el mismo
|
||||
nombre en la esquina superior izquierda.
|
||||
Si has cambiado tu preferencia de núcleo tras haber cargado la ROM, no te olvides de volverlo a cargar (atajo por defecto: Ctrl+R).
|
||||
3. Arrastra el archivo `Connector.lua` que has descargado a la ventana principal de EmuHawk.
|
||||
- Busca en la carpeta de Archipelago `/SNI/lua/`.
|
||||
- También podrías abrir la consola de Lua manualmente, hacer click en `Script` 〉 `Open Script`, e ir a `Connector.lua`
|
||||
con el selector de archivos.
|
||||
|
||||
##### RetroArch 1.10.1 o más nuevo
|
||||
|
||||
Sólo hay que segiur estos pasos una vez.
|
||||
Sólo hay que seguir estos pasos una vez.
|
||||
|
||||
1. Comienza en la pantalla del menú principal de RetroArch.
|
||||
2. Ve a Ajustes --> Interfaz de usario. Configura "Mostrar ajustes avanzados" en ON.
|
||||
3. Ve a Ajustes --> Red. Configura "Comandos de red" en ON. (Se encuentra bajo Request Device 16.) Deja en 55355 (el
|
||||
default) el Puerto de comandos de red.
|
||||
3. Ve a Ajustes --> Red. Pon "Comandos de red" en ON. (Se encuentra bajo Request Device 16.) Deja en 55355 el valor por defecto,
|
||||
el Puerto de comandos de red.
|
||||
|
||||

|
||||
4. Ve a Menú principal --> Actualizador en línea --> Descargador de núcleos. Desplázate y selecciona "Nintendo - SNES /
|
||||
SFC (bsnes-mercury Performance)".
|
||||
|
||||
Cuando cargas un ROM, asegúrate de seleccionar un núcleo **bsnes-mercury**. Estos son los sólos núcleos que permiten
|
||||
Cuando cargas un ROM, asegúrate de seleccionar un núcleo **bsnes-mercury**. Estos son los únicos núcleos que permiten
|
||||
que herramientas externas lean datos del ROM.
|
||||
|
||||
#### Con Hardware
|
||||
|
||||
Esta guía asume que ya has descargado el firmware correcto para tu dispositivo. Si no lo has hecho ya, hazlo ahora. Los
|
||||
Esta guía asume que ya has descargado el firmware correcto para tu dispositivo. Si no lo has hecho ya, por favor hazlo ahora. Los
|
||||
usuarios de SD2SNES y FXPak Pro pueden descargar el firmware apropiado
|
||||
[aqui](https://github.com/RedGuyyyy/sd2snes/releases). Los usuarios de otros dispositivos pueden encontrar información
|
||||
[aqui](https://github.com/RedGuyyyy/sd2snes/releases). Puede que los usuarios de otros dispositivos encuentren informacion útil
|
||||
[en esta página](http://usb2snes.com/#supported-platforms).
|
||||
|
||||
1. Cierra tu emulador, el cual debe haberse autoejecutado.
|
||||
2. Cierra QUsb2Snes, el cual fue ejecutado junto al cliente.
|
||||
3. Ejecuta la version correcta de QUsb2Snes (v0.7.16).
|
||||
4. Enciende tu dispositivo y carga la ROM.
|
||||
5. Observa en el cliente que ahora muestra "SNES Device: Connected", y aparece el nombre del dispositivo.
|
||||
2. Enciende tu dispositivo y carga la ROM.
|
||||
|
||||
### Conecta al MultiServer
|
||||
### Conecta al Servidor Archipelago
|
||||
|
||||
El fichero de parche que ha lanzado el cliente debe haberte conectado automaticamente al MultiServer. Hay algunas
|
||||
razonas por las que esto puede que no pase, incluyendo que el juego este hospedado en el sitio web pero se genero en
|
||||
algún otro sitio. Si el cliente muestra "Server Status: Not Connected", preguntale al creador de la partida la dirección
|
||||
del servidor, copiala en el campo "Server" y presiona Enter.
|
||||
El fichero de parche que ha lanzado el cliente debería de haberte conectado automaticamente al MultiServer. Sin embargo hay algunas
|
||||
razones por las que puede que esto no suceda, como que la partida este hospedada en la página web pero generada en otra parte. Si la
|
||||
ventana del cliente muestra "Server Status: Not Connected", simplemente preguntale al creador de la partida la dirección
|
||||
del servidor, cópiala en el campo "Server" y presiona Enter.
|
||||
|
||||
El cliente intentara conectarse a esta nueva dirección, y debería mostrar "Server Status: Connected" en algún momento.
|
||||
Si el cliente no se conecta al cabo de un rato, puede ser que necesites refrescar la pagina web.
|
||||
El cliente intentará conectarse a esta nueva dirección, y debería mostrar "Server Status: Connected" momentáneamente.
|
||||
|
||||
### Jugando
|
||||
### Jugar al juego
|
||||
|
||||
Cuando ambos SNES Device and Server aparezcan como "connected", estas listo para empezar a jugar. Felicidades por unirte
|
||||
satisfactoriamente a una partida de multiworld!
|
||||
|
||||
## Hospedando una partida de multiworld
|
||||
|
||||
La manera recomendad para hospedar una partida es usar el servicio proveído en
|
||||
[el sitio web](/generate). El proceso es relativamente sencillo:
|
||||
|
||||
1. Recolecta los ficheros YAML de todos los jugadores que participen.
|
||||
2. Crea un fichero ZIP conteniendo esos ficheros.
|
||||
3. Carga el fichero zip en el sitio web enlazado anteriormente.
|
||||
4. Espera a que la seed sea generada.
|
||||
5. Cuando esto acabe, se te redigirá a una pagina titulada "Seed Info".
|
||||
6. Haz click en "Create New Room". Esto te llevara a la pagina del servidor. Pasa el enlace a esta pagina a los
|
||||
jugadores para que puedan descargar los ficheros de parche de ahi.
|
||||
**Nota:** Los ficheros de parche de esta pagina permiten a los jugadores conectarse al servidor automaticamente,
|
||||
mientras que los de la pagina "Seed info" no.
|
||||
7. Hay un enlace a un MultiWorld Tracker en la parte superior de la pagina de la sala. Deberías pasar también este
|
||||
enlace a los jugadores para que puedan ver el progreso de la partida. A los observadores también se les puede pasar
|
||||
este enlace.
|
||||
8. Una vez todos los jugadores se han unido, podeis empezar a jugar.
|
||||
|
||||
## Auto-Tracking
|
||||
|
||||
Si deseas usar auto-tracking para tu partida, varios programas ofrecen esta funcionalidad.
|
||||
El programa recomentdado actualmente es:
|
||||
[OpenTracker](https://github.com/trippsc2/OpenTracker/releases).
|
||||
|
||||
### Instalación
|
||||
|
||||
1. Descarga el fichero de instalacion apropiado para tu ordenador (Usuarios de windows quieren el fichero ".msi").
|
||||
2. Durante el proceso de insatalación, puede que se te pida instalar Microsoft Visual Studio Build Tools. Un enlace este
|
||||
programa se muestra durante la proceso, y debe ser ejecutado manualmente.
|
||||
|
||||
### Activar auto-tracking
|
||||
|
||||
1. Con OpenTracker ejecutado, haz click en el menu Tracking en la parte superior de la ventana, y elige **
|
||||
AutoTracker...**
|
||||
2. Click the **Get Devices** button
|
||||
3. Selecciona tu "SNES device" de la lista
|
||||
4. Si quieres que las llaves y los objetos de mazmorra tambien sean marcados, activa la caja con nombre **Race Illegal
|
||||
Tracking**
|
||||
5. Haz click en el boton **Start Autotracking**
|
||||
6. Cierra la ventana AutoTracker, ya que deja de ser necesaria
|
||||
Cuando el cliente muestre tanto el dispositivo SNES como el servidor como conectados, estas listo para empezar a jugar. Felicidades por
|
||||
haberte unido a una partida multiworld con exito! Puedes ejecutar varios comandos en tu cliente. Para mas informacion
|
||||
acerca de estos comando puedes usar `/help` para comandos locales del cliente y `!help` para comandos de servidor.
|
||||
|
||||
@@ -130,19 +130,21 @@ class TestGanonsTower(TestDungeon):
|
||||
|
||||
["Ganons Tower - Pre-Moldorm Chest", False, []],
|
||||
["Ganons Tower - Pre-Moldorm Chest", False, [], ['Progressive Bow']],
|
||||
["Ganons Tower - Pre-Moldorm Chest", False, [], ['Bomb Upgrade (50)']],
|
||||
["Ganons Tower - Pre-Moldorm Chest", False, [], ['Big Key (Ganons Tower)']],
|
||||
["Ganons Tower - Pre-Moldorm Chest", False, [], ['Lamp', 'Fire Rod']],
|
||||
["Ganons Tower - Pre-Moldorm Chest", True, ['Progressive Bow', 'Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Lamp']],
|
||||
["Ganons Tower - Pre-Moldorm Chest", True, ['Progressive Bow', 'Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Fire Rod']],
|
||||
["Ganons Tower - Pre-Moldorm Chest", True, ['Bomb Upgrade (50)', 'Progressive Bow', 'Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Lamp']],
|
||||
["Ganons Tower - Pre-Moldorm Chest", True, ['Bomb Upgrade (50)', 'Progressive Bow', 'Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Fire Rod']],
|
||||
|
||||
["Ganons Tower - Validation Chest", False, []],
|
||||
["Ganons Tower - Validation Chest", False, [], ['Hookshot']],
|
||||
["Ganons Tower - Validation Chest", False, [], ['Progressive Bow']],
|
||||
["Ganons Tower - Validation Chest", False, [], ['Bomb Upgrade (50)']],
|
||||
["Ganons Tower - Validation Chest", False, [], ['Big Key (Ganons Tower)']],
|
||||
["Ganons Tower - Validation Chest", False, [], ['Lamp', 'Fire Rod']],
|
||||
["Ganons Tower - Validation Chest", False, [], ['Progressive Sword', 'Hammer']],
|
||||
["Ganons Tower - Validation Chest", True, ['Progressive Bow', 'Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Lamp', 'Hookshot', 'Progressive Sword']],
|
||||
["Ganons Tower - Validation Chest", True, ['Progressive Bow', 'Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Fire Rod', 'Hookshot', 'Progressive Sword']],
|
||||
["Ganons Tower - Validation Chest", True, ['Progressive Bow', 'Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Lamp', 'Hookshot', 'Hammer']],
|
||||
["Ganons Tower - Validation Chest", True, ['Progressive Bow', 'Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Fire Rod', 'Hookshot', 'Hammer']],
|
||||
["Ganons Tower - Validation Chest", True, ['Bomb Upgrade (50)', 'Progressive Bow', 'Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Lamp', 'Hookshot', 'Progressive Sword']],
|
||||
["Ganons Tower - Validation Chest", True, ['Bomb Upgrade (50)', 'Progressive Bow', 'Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Fire Rod', 'Hookshot', 'Progressive Sword']],
|
||||
["Ganons Tower - Validation Chest", True, ['Bomb Upgrade (50)', 'Progressive Bow', 'Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Lamp', 'Hookshot', 'Hammer']],
|
||||
["Ganons Tower - Validation Chest", True, ['Bomb Upgrade (50)', 'Progressive Bow', 'Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Fire Rod', 'Hookshot', 'Hammer']],
|
||||
])
|
||||
@@ -77,5 +77,5 @@ class TestMiseryMire(TestDungeon):
|
||||
["Misery Mire - Boss", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
|
||||
["Misery Mire - Boss", True, ['Bomb Upgrade (+5)', 'Big Key (Misery Mire)', 'Lamp', 'Cane of Somaria', 'Progressive Sword', 'Pegasus Boots']],
|
||||
["Misery Mire - Boss", True, ['Bomb Upgrade (+5)', 'Big Key (Misery Mire)', 'Lamp', 'Cane of Somaria', 'Hammer', 'Pegasus Boots']],
|
||||
["Misery Mire - Boss", True, ['Bomb Upgrade (+5)', 'Big Key (Misery Mire)', 'Lamp', 'Cane of Somaria', 'Progressive Bow', 'Pegasus Boots']],
|
||||
["Misery Mire - Boss", True, ['Bomb Upgrade (+5)', 'Big Key (Misery Mire)', 'Lamp', 'Cane of Somaria', 'Progressive Bow', 'Arrow Upgrade (+5)', 'Pegasus Boots']],
|
||||
])
|
||||
@@ -93,7 +93,7 @@ class AquariaWorld(World):
|
||||
options: AquariaOptions
|
||||
"Every options of the world"
|
||||
|
||||
regions: AquariaRegions
|
||||
regions: AquariaRegions | None
|
||||
"Used to manage Regions"
|
||||
|
||||
exclude: List[str]
|
||||
@@ -101,10 +101,17 @@ class AquariaWorld(World):
|
||||
def __init__(self, multiworld: MultiWorld, player: int):
|
||||
"""Initialisation of the Aquaria World"""
|
||||
super(AquariaWorld, self).__init__(multiworld, player)
|
||||
self.regions = AquariaRegions(multiworld, player)
|
||||
self.regions = None
|
||||
self.ingredients_substitution = []
|
||||
self.exclude = []
|
||||
|
||||
def generate_early(self) -> None:
|
||||
"""
|
||||
Run before any general steps of the MultiWorld other than options. Useful for getting and adjusting option
|
||||
results and determining layouts for entrance rando etc. start inventory gets pushed after this step.
|
||||
"""
|
||||
self.regions = AquariaRegions(self.multiworld, self.player)
|
||||
|
||||
def create_regions(self) -> None:
|
||||
"""
|
||||
Create every Region in `regions`
|
||||
|
||||
@@ -4,14 +4,17 @@ import random
|
||||
|
||||
|
||||
class ChoiceIsRandom(Choice):
|
||||
randomized: bool = False
|
||||
randomized: bool
|
||||
|
||||
def __init__(self, value: int, randomized: bool = False):
|
||||
super().__init__(value)
|
||||
self.randomized = randomized
|
||||
|
||||
@classmethod
|
||||
def from_text(cls, text: str) -> Choice:
|
||||
text = text.lower()
|
||||
if text == "random":
|
||||
cls.randomized = True
|
||||
return cls(random.choice(list(cls.name_lookup)))
|
||||
return cls(random.choice(list(cls.name_lookup)), True)
|
||||
for option_name, value in cls.options.items():
|
||||
if option_name == text:
|
||||
return cls(value)
|
||||
|
||||
@@ -103,6 +103,9 @@ class BlasphemousWorld(World):
|
||||
if not self.options.wall_climb_shuffle:
|
||||
self.multiworld.push_precollected(self.create_item("Wall Climb Ability"))
|
||||
|
||||
if self.options.thorn_shuffle == "local_only":
|
||||
self.options.local_items.value.add("Thorn Upgrade")
|
||||
|
||||
if not self.options.boots_of_pleading:
|
||||
self.disabled_locations.append("RE401")
|
||||
|
||||
@@ -200,9 +203,6 @@ class BlasphemousWorld(World):
|
||||
|
||||
if not self.options.skill_randomizer:
|
||||
self.place_items_from_dict(skill_dict)
|
||||
|
||||
if self.options.thorn_shuffle == "local_only":
|
||||
self.options.local_items.value.add("Thorn Upgrade")
|
||||
|
||||
|
||||
def place_items_from_set(self, location_set: Set[str], name: str):
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import logging
|
||||
import asyncio
|
||||
|
||||
from NetUtils import ClientStatus, color
|
||||
from worlds.AutoSNIClient import SNIClient
|
||||
@@ -32,7 +31,7 @@ class DKC3SNIClient(SNIClient):
|
||||
|
||||
|
||||
async def validate_rom(self, ctx):
|
||||
from SNIClient import snes_buffered_write, snes_flush_writes, snes_read
|
||||
from SNIClient import snes_read
|
||||
|
||||
rom_name = await snes_read(ctx, DKC3_ROMHASH_START, ROMHASH_SIZE)
|
||||
if rom_name is None or rom_name == bytes([0] * ROMHASH_SIZE) or rom_name[:2] != b"D3":
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import typing
|
||||
|
||||
from BaseClasses import Item, ItemClassification
|
||||
from BaseClasses import Item
|
||||
from .Names import ItemName
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from dataclasses import dataclass
|
||||
import typing
|
||||
|
||||
from Options import Choice, Range, Toggle, DeathLink, DefaultOnToggle, OptionGroup, PerGameCommonOptions
|
||||
from Options import Choice, Range, Toggle, DefaultOnToggle, OptionGroup, PerGameCommonOptions
|
||||
|
||||
|
||||
class Goal(Choice):
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import typing
|
||||
|
||||
from BaseClasses import MultiWorld, Region, Entrance
|
||||
from .Items import DKC3Item
|
||||
from BaseClasses import Region, Entrance
|
||||
from worlds.AutoWorld import World
|
||||
from .Locations import DKC3Location
|
||||
from .Names import LocationName, ItemName
|
||||
from worlds.AutoWorld import World
|
||||
|
||||
|
||||
def create_regions(world: World, active_locations):
|
||||
|
||||
@@ -2,7 +2,6 @@ import Utils
|
||||
from Utils import read_snes_rom
|
||||
from worlds.AutoWorld import World
|
||||
from worlds.Files import APDeltaPatch
|
||||
from .Locations import lookup_id_to_name, all_locations
|
||||
from .Levels import level_list, level_dict
|
||||
|
||||
USHASH = '120abf304f0c40fe059f6a192ed4f947'
|
||||
@@ -436,7 +435,7 @@ level_music_ids = [
|
||||
|
||||
class LocalRom:
|
||||
|
||||
def __init__(self, file, patch=True, vanillaRom=None, name=None, hash=None):
|
||||
def __init__(self, file, name=None, hash=None):
|
||||
self.name = name
|
||||
self.hash = hash
|
||||
self.orig_buffer = None
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import math
|
||||
|
||||
from worlds.AutoWorld import World
|
||||
from worlds.generic.Rules import add_rule
|
||||
from .Names import LocationName, ItemName
|
||||
from worlds.AutoWorld import LogicMixin, World
|
||||
from worlds.generic.Rules import add_rule, set_rule
|
||||
|
||||
|
||||
def set_rules(world: World):
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
import dataclasses
|
||||
import os
|
||||
import typing
|
||||
import math
|
||||
import os
|
||||
import threading
|
||||
import typing
|
||||
|
||||
import settings
|
||||
from BaseClasses import Item, MultiWorld, Tutorial, ItemClassification
|
||||
from Options import PerGameCommonOptions
|
||||
import Patch
|
||||
import settings
|
||||
from worlds.AutoWorld import WebWorld, World
|
||||
|
||||
from .Client import DKC3SNIClient
|
||||
from .Items import DKC3Item, ItemData, item_table, inventory_table, junk_table
|
||||
from .Levels import level_list
|
||||
|
||||
@@ -234,8 +234,7 @@ async def game_watcher(ctx: FactorioContext):
|
||||
f"Connected Multiworld is not the expected one {data['seed_name']} != {ctx.seed_name}")
|
||||
else:
|
||||
data = data["info"]
|
||||
research_data = data["research_done"]
|
||||
research_data = {int(tech_name.split("-")[1]) for tech_name in research_data}
|
||||
research_data: set[int] = {int(tech_name.split("-")[1]) for tech_name in data["research_done"]}
|
||||
victory = data["victory"]
|
||||
await ctx.update_death_link(data["death_link"])
|
||||
ctx.multiplayer = data.get("multiplayer", False)
|
||||
@@ -249,7 +248,7 @@ async def game_watcher(ctx: FactorioContext):
|
||||
f"New researches done: "
|
||||
f"{[ctx.location_names.lookup_in_game(rid) for rid in research_data - ctx.locations_checked]}")
|
||||
ctx.locations_checked = research_data
|
||||
await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": tuple(research_data)}])
|
||||
await ctx.check_locations(research_data)
|
||||
death_link_tick = data.get("death_link_tick", 0)
|
||||
if death_link_tick != ctx.death_link_tick:
|
||||
ctx.death_link_tick = death_link_tick
|
||||
|
||||
@@ -37,8 +37,8 @@ base_info = {
|
||||
"description": "Integration client for the Archipelago Randomizer",
|
||||
"factorio_version": "2.0",
|
||||
"dependencies": [
|
||||
"base >= 2.0.15",
|
||||
"? quality >= 2.0.15",
|
||||
"base >= 2.0.28",
|
||||
"? quality >= 2.0.28",
|
||||
"! space-age",
|
||||
"? science-not-invited",
|
||||
"? factory-levels"
|
||||
|
||||
@@ -3,13 +3,23 @@ from __future__ import annotations
|
||||
from dataclasses import dataclass
|
||||
import typing
|
||||
|
||||
from schema import Schema, Optional, And, Or
|
||||
from schema import Schema, Optional, And, Or, SchemaError
|
||||
|
||||
from Options import Choice, OptionDict, OptionSet, DefaultOnToggle, Range, DeathLink, Toggle, \
|
||||
StartInventoryPool, PerGameCommonOptions, OptionGroup
|
||||
|
||||
# schema helpers
|
||||
FloatRange = lambda low, high: And(Or(int, float), lambda f: low <= f <= high)
|
||||
class FloatRange:
|
||||
def __init__(self, low, high):
|
||||
self._low = low
|
||||
self._high = high
|
||||
|
||||
def validate(self, value):
|
||||
if not isinstance(value, (float, int)):
|
||||
raise SchemaError(f"should be instance of float or int, but was {value!r}")
|
||||
if not self._low <= value <= self._high:
|
||||
raise SchemaError(f"{value} is not between {self._low} and {self._high}")
|
||||
|
||||
LuaBool = Or(bool, And(int, lambda n: n in (0, 1)))
|
||||
|
||||
|
||||
|
||||
@@ -63,17 +63,19 @@ class FactorioElement:
|
||||
|
||||
|
||||
class Technology(FactorioElement): # maybe make subclass of Location?
|
||||
has_modifier: bool
|
||||
factorio_id: int
|
||||
progressive: Tuple[str]
|
||||
unlocks: Union[Set[str], bool] # bool case is for progressive technologies
|
||||
modifiers: list[str]
|
||||
|
||||
def __init__(self, technology_name: str, factorio_id: int, progressive: Tuple[str] = (),
|
||||
has_modifier: bool = False, unlocks: Union[Set[str], bool] = None):
|
||||
modifiers: list[str] = None, unlocks: Union[Set[str], bool] = None):
|
||||
self.name = technology_name
|
||||
self.factorio_id = factorio_id
|
||||
self.progressive = progressive
|
||||
self.has_modifier = has_modifier
|
||||
if modifiers is None:
|
||||
modifiers = []
|
||||
self.modifiers = modifiers
|
||||
if unlocks:
|
||||
self.unlocks = unlocks
|
||||
else:
|
||||
@@ -82,6 +84,10 @@ class Technology(FactorioElement): # maybe make subclass of Location?
|
||||
def __hash__(self):
|
||||
return self.factorio_id
|
||||
|
||||
@property
|
||||
def has_modifier(self) -> bool:
|
||||
return bool(self.modifiers)
|
||||
|
||||
def get_custom(self, world, allowed_packs: Set[str], player: int) -> CustomTechnology:
|
||||
return CustomTechnology(self, world, allowed_packs, player)
|
||||
|
||||
@@ -191,13 +197,14 @@ class Machine(FactorioElement):
|
||||
|
||||
|
||||
recipe_sources: Dict[str, Set[str]] = {} # recipe_name -> technology source
|
||||
mining_with_fluid_sources: set[str] = set()
|
||||
|
||||
# recipes and technologies can share names in Factorio
|
||||
for technology_name, data in sorted(techs_future.result().items()):
|
||||
technology = Technology(
|
||||
technology_name,
|
||||
factorio_tech_id,
|
||||
has_modifier=data["has_modifier"],
|
||||
modifiers=data.get("modifiers", []),
|
||||
unlocks=set(data["unlocks"]) - start_unlocked_recipes,
|
||||
)
|
||||
factorio_tech_id += 1
|
||||
@@ -205,7 +212,8 @@ for technology_name, data in sorted(techs_future.result().items()):
|
||||
technology_table[technology_name] = technology
|
||||
for recipe_name in technology.unlocks:
|
||||
recipe_sources.setdefault(recipe_name, set()).add(technology_name)
|
||||
|
||||
if "mining-with-fluid" in technology.modifiers:
|
||||
mining_with_fluid_sources.add(technology_name)
|
||||
del techs_future
|
||||
|
||||
recipes = {}
|
||||
@@ -221,6 +229,8 @@ for resource_name, resource_data in resources_future.result().items():
|
||||
"energy": resource_data["mining_time"],
|
||||
"category": resource_data["category"]
|
||||
}
|
||||
if "required_fluid" in resource_data:
|
||||
recipe_sources.setdefault(f"mining-{resource_name}", set()).update(mining_with_fluid_sources)
|
||||
del resources_future
|
||||
|
||||
for recipe_name, recipe_data in raw_recipes.items():
|
||||
@@ -431,7 +441,9 @@ for root in sorted_rows:
|
||||
factorio_tech_id += 1
|
||||
progressive_technology = Technology(root, factorio_tech_id,
|
||||
tuple(progressive),
|
||||
has_modifier=any(technology_table[tech].has_modifier for tech in progressive),
|
||||
modifiers=sorted(set.union(
|
||||
*(set(technology_table[tech].modifiers) for tech in progressive)
|
||||
)),
|
||||
unlocks=any(technology_table[tech].unlocks for tech in progressive),)
|
||||
progressive_tech_table[root] = progressive_technology.factorio_id
|
||||
progressive_technology_table[root] = progressive_technology
|
||||
|
||||
@@ -445,6 +445,10 @@ end
|
||||
|
||||
script.on_event(defines.events.on_player_main_inventory_changed, update_player_event)
|
||||
|
||||
-- Update players when the cutscene is cancelled or finished. (needed for skins_factored)
|
||||
script.on_event(defines.events.on_cutscene_cancelled, update_player_event)
|
||||
script.on_event(defines.events.on_cutscene_finished, update_player_event)
|
||||
|
||||
function add_samples(force, name, count)
|
||||
local function add_to_table(t)
|
||||
if count <= 0 then
|
||||
@@ -713,8 +717,10 @@ TRAP_TABLE = {
|
||||
game.surfaces["nauvis"].build_enemy_base(game.forces["player"].get_spawn_position(game.get_surface(1)), 25)
|
||||
end,
|
||||
["Evolution Trap"] = function ()
|
||||
game.forces["enemy"].evolution_factor = game.forces["enemy"].evolution_factor + (TRAP_EVO_FACTOR * (1 - game.forces["enemy"].evolution_factor))
|
||||
game.print({"", "New evolution factor:", game.forces["enemy"].evolution_factor})
|
||||
local new_factor = game.forces["enemy"].get_evolution_factor("nauvis") +
|
||||
(TRAP_EVO_FACTOR * (1 - game.forces["enemy"].get_evolution_factor("nauvis")))
|
||||
game.forces["enemy"].set_evolution_factor(new_factor, "nauvis")
|
||||
game.print({"", "New evolution factor:", new_factor})
|
||||
end,
|
||||
["Teleport Trap"] = function ()
|
||||
for _, player in ipairs(game.forces["player"].players) do
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{% from "macros.lua" import dict_to_recipe, variable_to_lua %}
|
||||
-- this file gets written automatically by the Archipelago Randomizer and is in its raw form a Jinja2 Template
|
||||
require('lib')
|
||||
data.raw["item"]["rocket-part"].hidden = false
|
||||
data.raw["rocket-silo"]["rocket-silo"].fluid_boxes = {
|
||||
{
|
||||
production_type = "input",
|
||||
@@ -162,6 +163,7 @@ data.raw["ammo"]["artillery-shell"].stack_size = 10
|
||||
{# each randomized tech gets set to be invisible, with new nodes added that trigger those #}
|
||||
{%- for original_tech_name in base_tech_table -%}
|
||||
technologies["{{ original_tech_name }}"].hidden = true
|
||||
technologies["{{ original_tech_name }}"].hidden_in_factoriopedia = true
|
||||
{% endfor %}
|
||||
{%- for location, item in locations %}
|
||||
{#- the tech researched by the local player #}
|
||||
|
||||
File diff suppressed because one or more lines are too long
39
worlds/factorio/test_file_validation.py
Normal file
39
worlds/factorio/test_file_validation.py
Normal file
@@ -0,0 +1,39 @@
|
||||
"""Tests for error messages from YAML validation."""
|
||||
|
||||
import os
|
||||
import unittest
|
||||
|
||||
import WebHostLib.check
|
||||
|
||||
FACTORIO_YAML="""
|
||||
game: Factorio
|
||||
Factorio:
|
||||
world_gen:
|
||||
autoplace_controls:
|
||||
coal:
|
||||
richness: 1
|
||||
frequency: {}
|
||||
size: 1
|
||||
"""
|
||||
|
||||
def yamlWithFrequency(f):
|
||||
return FACTORIO_YAML.format(f)
|
||||
|
||||
|
||||
class TestFileValidation(unittest.TestCase):
|
||||
def test_out_of_range(self):
|
||||
results, _ = WebHostLib.check.roll_options({"bob.yaml": yamlWithFrequency(1000)})
|
||||
self.assertIn("between 0 and 6", results["bob.yaml"])
|
||||
|
||||
def test_bad_non_numeric(self):
|
||||
results, _ = WebHostLib.check.roll_options({"bob.yaml": yamlWithFrequency("not numeric")})
|
||||
self.assertIn("float", results["bob.yaml"])
|
||||
self.assertIn("int", results["bob.yaml"])
|
||||
|
||||
def test_good_float(self):
|
||||
results, _ = WebHostLib.check.roll_options({"bob.yaml": yamlWithFrequency(1.0)})
|
||||
self.assertIs(results["bob.yaml"], True)
|
||||
|
||||
def test_good_int(self):
|
||||
results, _ = WebHostLib.check.roll_options({"bob.yaml": yamlWithFrequency(1)})
|
||||
self.assertIs(results["bob.yaml"], True)
|
||||
@@ -44,8 +44,13 @@ class FaxanaduWorld(World):
|
||||
location_name_to_id = {loc.name: loc.id for loc in Locations.locations if loc.id is not None}
|
||||
|
||||
def __init__(self, world: MultiWorld, player: int):
|
||||
self.filler_ratios: Dict[str, int] = {}
|
||||
|
||||
self.filler_ratios: Dict[str, int] = {
|
||||
item.name: item.count
|
||||
for item in Items.items
|
||||
if item.classification in [ItemClassification.filler, ItemClassification.trap]
|
||||
}
|
||||
# Remove poison by default to respect itemlinking
|
||||
self.filler_ratios["Poison"] = 0
|
||||
super().__init__(world, player)
|
||||
|
||||
def create_regions(self):
|
||||
@@ -160,19 +165,13 @@ class FaxanaduWorld(World):
|
||||
for i in range(item.progression_count):
|
||||
itempool.append(FaxanaduItem(item.name, ItemClassification.progression, item.id, self.player))
|
||||
|
||||
# Set up filler ratios
|
||||
self.filler_ratios = {
|
||||
item.name: item.count
|
||||
for item in Items.items
|
||||
if item.classification in [ItemClassification.filler, ItemClassification.trap]
|
||||
}
|
||||
|
||||
# Adjust filler ratios
|
||||
# If red potions are locked in shops, remove the count from the ratio.
|
||||
self.filler_ratios["Red Potion"] -= red_potion_in_shop_count
|
||||
|
||||
# Remove poisons if not desired
|
||||
if not self.options.include_poisons:
|
||||
self.filler_ratios["Poison"] = 0
|
||||
# Add poisons if desired
|
||||
if self.options.include_poisons:
|
||||
self.filler_ratios["Poison"] = self.item_name_to_item["Poison"].count
|
||||
|
||||
# Randomly add fillers to the pool with ratios based on og game occurrence counts.
|
||||
filler_count = len(Locations.locations) - len(itempool) - prefilled_count
|
||||
|
||||
@@ -260,7 +260,8 @@ def create_items(self) -> None:
|
||||
items.append(i)
|
||||
|
||||
for item_group in ("Key Items", "Spells", "Armors", "Helms", "Shields", "Accessories", "Weapons"):
|
||||
for item in self.item_name_groups[item_group]:
|
||||
# Sort for deterministic order
|
||||
for item in sorted(self.item_name_groups[item_group]):
|
||||
add_item(item)
|
||||
|
||||
if self.options.brown_boxes == "include":
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from Options import Choice, FreeText, Toggle, Range, PerGameCommonOptions
|
||||
from Options import Choice, FreeText, ItemsAccessibility, Toggle, Range, PerGameCommonOptions
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@@ -324,6 +324,7 @@ class KaelisMomFightsMinotaur(Toggle):
|
||||
|
||||
@dataclass
|
||||
class FFMQOptions(PerGameCommonOptions):
|
||||
accessibility: ItemsAccessibility
|
||||
logic: Logic
|
||||
brown_boxes: BrownBoxes
|
||||
sky_coin_mode: SkyCoinMode
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Final Fantasy Mystic Quest
|
||||
|
||||
## Game page in other languages:
|
||||
* [Français](/games/Final%20Fantasy%20Mystic%20Quest/info/fr)
|
||||
* [Français](/games/Final%20Fantasy%20Mystic%20Quest/info/fr)
|
||||
|
||||
## Where is the options page?
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ including the exclamation point.
|
||||
- `!countdown <number of seconds>` Starts a countdown using the given seconds value. Useful for synchronizing starts.
|
||||
Defaults to 10 seconds if no argument is provided.
|
||||
- `!alias <alias>` Sets your alias, which allows you to use commands with the alias rather than your provided name.
|
||||
`!alias` on its own will reset the alias to the player's original name.
|
||||
- `!admin <command>` Executes a command as if you typed it into the server console. Remote administration must be
|
||||
enabled.
|
||||
|
||||
@@ -65,6 +66,7 @@ including the exclamation point.
|
||||
argument is provided.
|
||||
- `/option <option name> <option value>` Set a server option. For a list of options, use the `/options` command.
|
||||
- `/alias <player name> <alias name>` Assign a player an alias, allowing you to reference the player by the alias in commands.
|
||||
`!alias <player name>` on its own will reset the alias to the player's original name.
|
||||
|
||||
|
||||
### Collect/Release
|
||||
|
||||
@@ -132,7 +132,13 @@ splitter_pattern = re.compile(r'(?<!^)(?=[A-Z])')
|
||||
for option_name, option_data in pool_options.items():
|
||||
extra_data = {"__module__": __name__, "items": option_data[0], "locations": option_data[1]}
|
||||
if option_name in option_docstrings:
|
||||
extra_data["__doc__"] = option_docstrings[option_name]
|
||||
if option_name == "RandomizeFocus":
|
||||
# pool options for focus are just lying
|
||||
count = 1
|
||||
else:
|
||||
count = len([loc for loc in option_data[1] if loc != "Start"])
|
||||
extra_data["__doc__"] = option_docstrings[option_name] + \
|
||||
f"\n This option adds approximately {count} location{'s' if count != 1 else ''}."
|
||||
if option_name in default_on:
|
||||
option = type(option_name, (DefaultOnToggle,), extra_data)
|
||||
else:
|
||||
@@ -213,6 +219,7 @@ class MaximumEssencePrice(MinimumEssencePrice):
|
||||
class MinimumEggPrice(Range):
|
||||
"""The minimum rancid egg price in the range of prices that an item should cost from Jiji.
|
||||
Only takes effect if the EggSlotShops option is greater than 0."""
|
||||
rich_text_doc = False
|
||||
display_name = "Minimum Egg Price"
|
||||
range_start = 1
|
||||
range_end = 20
|
||||
@@ -222,6 +229,7 @@ class MinimumEggPrice(Range):
|
||||
class MaximumEggPrice(MinimumEggPrice):
|
||||
"""The maximum rancid egg price in the range of prices that an item should cost from Jiji.
|
||||
Only takes effect if the EggSlotShops option is greater than 0."""
|
||||
rich_text_doc = False
|
||||
display_name = "Maximum Egg Price"
|
||||
default = 10
|
||||
|
||||
@@ -265,6 +273,7 @@ class RandomCharmCosts(NamedRange):
|
||||
Set to -1 or vanilla for vanilla costs.
|
||||
Set to -2 or shuffle to shuffle around the vanilla costs to different charms."""
|
||||
|
||||
rich_text_doc = False
|
||||
display_name = "Randomize Charm Notch Costs"
|
||||
range_start = 0
|
||||
range_end = 240
|
||||
@@ -437,6 +446,7 @@ class Goal(Choice):
|
||||
class GrubHuntGoal(NamedRange):
|
||||
"""The amount of grubs required to finish Grub Hunt.
|
||||
On 'All' any grubs from item links replacements etc. will be counted"""
|
||||
rich_text_doc = False
|
||||
display_name = "Grub Hunt Goal"
|
||||
range_start = 1
|
||||
range_end = 46
|
||||
@@ -446,7 +456,7 @@ class GrubHuntGoal(NamedRange):
|
||||
|
||||
class WhitePalace(Choice):
|
||||
"""
|
||||
Whether or not to include White Palace or not. Note: Even if excluded, the King Fragment check may still be
|
||||
Whether or not to include White Palace or not. Note: Even if excluded, the King Fragment check may still be
|
||||
required if charms are vanilla.
|
||||
"""
|
||||
display_name = "White Palace"
|
||||
@@ -483,6 +493,7 @@ class DeathLinkShade(Choice):
|
||||
** Self-death shade behavior is not changed; if a self-death normally creates a shade in vanilla, it will override
|
||||
your existing shade, if any.
|
||||
"""
|
||||
rich_text_doc = False
|
||||
option_vanilla = 0
|
||||
option_shadeless = 1
|
||||
option_shade = 2
|
||||
@@ -497,6 +508,7 @@ class DeathLinkBreaksFragileCharms(Toggle):
|
||||
** Self-death fragile charm behavior is not changed; if a self-death normally breaks fragile charms in vanilla, it
|
||||
will continue to do so.
|
||||
"""
|
||||
rich_text_doc = False
|
||||
display_name = "Deathlink Breaks Fragile Charms"
|
||||
|
||||
|
||||
@@ -515,6 +527,7 @@ class CostSanity(Choice):
|
||||
|
||||
These costs can be in Geo (except Grubfather, Seer and Eggshop), Grubs, Charms, Essence and/or Rancid Eggs
|
||||
"""
|
||||
rich_text_doc = False
|
||||
option_off = 0
|
||||
alias_no = 0
|
||||
option_on = 1
|
||||
|
||||
@@ -134,7 +134,9 @@ shop_cost_types: typing.Dict[str, typing.Tuple[str, ...]] = {
|
||||
|
||||
|
||||
class HKWeb(WebWorld):
|
||||
setup_en = Tutorial(
|
||||
rich_text_options_doc = True
|
||||
|
||||
setup_en = Tutorial(
|
||||
"Mod Setup and Use Guide",
|
||||
"A guide to playing Hollow Knight with Archipelago.",
|
||||
"English",
|
||||
@@ -143,7 +145,7 @@ class HKWeb(WebWorld):
|
||||
["Ijwu"]
|
||||
)
|
||||
|
||||
setup_pt_br = Tutorial(
|
||||
setup_pt_br = Tutorial(
|
||||
setup_en.tutorial_name,
|
||||
setup_en.description,
|
||||
"Português Brasileiro",
|
||||
@@ -179,6 +181,7 @@ class HKWorld(World):
|
||||
charm_costs: typing.List[int]
|
||||
cached_filler_items = {}
|
||||
grub_count: int
|
||||
grub_player_count: typing.Dict[int, int]
|
||||
|
||||
def __init__(self, multiworld, player):
|
||||
super(HKWorld, self).__init__(multiworld, player)
|
||||
@@ -188,7 +191,6 @@ class HKWorld(World):
|
||||
self.ranges = {}
|
||||
self.created_shop_items = 0
|
||||
self.vanilla_shop_costs = deepcopy(vanilla_shop_costs)
|
||||
self.grub_count = 0
|
||||
|
||||
def generate_early(self):
|
||||
options = self.options
|
||||
@@ -202,7 +204,14 @@ class HKWorld(World):
|
||||
mini.value = min(mini.value, maxi.value)
|
||||
self.ranges[term] = mini.value, maxi.value
|
||||
self.multiworld.push_precollected(HKItem(starts[options.StartLocation.current_key],
|
||||
True, None, "Event", self.player))
|
||||
True, None, "Event", self.player))
|
||||
|
||||
# defaulting so completion condition isn't incorrect before pre_fill
|
||||
self.grub_count = (
|
||||
46 if options.GrubHuntGoal == GrubHuntGoal.special_range_names["all"]
|
||||
else options.GrubHuntGoal
|
||||
)
|
||||
self.grub_player_count = {self.player: self.grub_count}
|
||||
|
||||
def white_palace_exclusions(self):
|
||||
exclusions = set()
|
||||
@@ -467,25 +476,20 @@ class HKWorld(World):
|
||||
elif goal == Goal.option_godhome_flower:
|
||||
multiworld.completion_condition[player] = lambda state: state.count("Godhome_Flower_Quest", player)
|
||||
elif goal == Goal.option_grub_hunt:
|
||||
pass # will set in stage_pre_fill()
|
||||
multiworld.completion_condition[player] = lambda state: self.can_grub_goal(state)
|
||||
else:
|
||||
# Any goal
|
||||
multiworld.completion_condition[player] = lambda state: _hk_siblings_ending(state, player) and \
|
||||
_hk_can_beat_radiance(state, player) and state.count("Godhome_Flower_Quest", player)
|
||||
_hk_can_beat_radiance(state, player) and state.count("Godhome_Flower_Quest", player) and \
|
||||
self.can_grub_goal(state)
|
||||
|
||||
set_rules(self)
|
||||
|
||||
def can_grub_goal(self, state: CollectionState) -> bool:
|
||||
return all(state.has("Grub", owner, count) for owner, count in self.grub_player_count.items())
|
||||
|
||||
@classmethod
|
||||
def stage_pre_fill(cls, multiworld: "MultiWorld"):
|
||||
def set_goal(player, grub_rule: typing.Callable[[CollectionState], bool]):
|
||||
world = multiworld.worlds[player]
|
||||
|
||||
if world.options.Goal == "grub_hunt":
|
||||
multiworld.completion_condition[player] = grub_rule
|
||||
else:
|
||||
old_rule = multiworld.completion_condition[player]
|
||||
multiworld.completion_condition[player] = lambda state: old_rule(state) and grub_rule(state)
|
||||
|
||||
worlds = [world for world in multiworld.get_game_worlds(cls.game) if world.options.Goal in ["any", "grub_hunt"]]
|
||||
if worlds:
|
||||
grubs = [item for item in multiworld.get_items() if item.name == "Grub"]
|
||||
@@ -523,13 +527,13 @@ class HKWorld(World):
|
||||
|
||||
for player, grub_player_count in per_player_grubs_per_player.items():
|
||||
if player in all_grub_players:
|
||||
set_goal(player, lambda state, g=grub_player_count: all(state.has("Grub", owner, count) for owner, count in g.items()))
|
||||
multiworld.worlds[player].grub_player_count = grub_player_count
|
||||
|
||||
for world in worlds:
|
||||
if world.player not in all_grub_players:
|
||||
world.grub_count = world.options.GrubHuntGoal.value
|
||||
player = world.player
|
||||
set_goal(player, lambda state, p=player, c=world.grub_count: state.has("Grub", p, c))
|
||||
world.grub_player_count = {player: world.grub_count}
|
||||
|
||||
def fill_slot_data(self):
|
||||
slot_data = {}
|
||||
|
||||
@@ -110,6 +110,7 @@ class KH2Context(CommonContext):
|
||||
18: TWTNW_Checks,
|
||||
# 255: {}, # starting screen
|
||||
}
|
||||
self.last_world_int = -1
|
||||
# 0x2A09C00+0x40 is the sve anchor. +1 is the last saved room
|
||||
# self.sveroom = 0x2A09C00 + 0x41
|
||||
# 0 not in battle 1 in yellow battle 2 red battle #short
|
||||
@@ -345,33 +346,12 @@ class KH2Context(CommonContext):
|
||||
self.lookup_id_to_item = {v: k for k, v in self.kh2_item_name_to_id.items()}
|
||||
self.ability_code_list = [self.kh2_item_name_to_id[item] for item in exclusion_item_table["Ability"]]
|
||||
|
||||
if "keyblade_abilities" in self.kh2slotdata.keys():
|
||||
sora_ability_dict = self.kh2slotdata["KeybladeAbilities"]
|
||||
if "KeybladeAbilities" in self.kh2slotdata.keys():
|
||||
# sora ability to slot
|
||||
self.AbilityQuantityDict.update(self.kh2slotdata["KeybladeAbilities"])
|
||||
# itemid:[slots that are available for that item]
|
||||
for k, v in sora_ability_dict.items():
|
||||
if v >= 1:
|
||||
if k not in self.sora_ability_to_slot.keys():
|
||||
self.sora_ability_to_slot[k] = []
|
||||
for _ in range(sora_ability_dict[k]):
|
||||
self.sora_ability_to_slot[k].append(self.kh2_seed_save_cache["SoraInvo"][0])
|
||||
self.kh2_seed_save_cache["SoraInvo"][0] -= 2
|
||||
donald_ability_dict = self.kh2slotdata["StaffAbilities"]
|
||||
for k, v in donald_ability_dict.items():
|
||||
if v >= 1:
|
||||
if k not in self.donald_ability_to_slot.keys():
|
||||
self.donald_ability_to_slot[k] = []
|
||||
for _ in range(donald_ability_dict[k]):
|
||||
self.donald_ability_to_slot[k].append(self.kh2_seed_save_cache["DonaldInvo"][0])
|
||||
self.kh2_seed_save_cache["DonaldInvo"][0] -= 2
|
||||
goofy_ability_dict = self.kh2slotdata["ShieldAbilities"]
|
||||
for k, v in goofy_ability_dict.items():
|
||||
if v >= 1:
|
||||
if k not in self.goofy_ability_to_slot.keys():
|
||||
self.goofy_ability_to_slot[k] = []
|
||||
for _ in range(goofy_ability_dict[k]):
|
||||
self.goofy_ability_to_slot[k].append(self.kh2_seed_save_cache["GoofyInvo"][0])
|
||||
self.kh2_seed_save_cache["GoofyInvo"][0] -= 2
|
||||
self.AbilityQuantityDict.update(self.kh2slotdata["StaffAbilities"])
|
||||
self.AbilityQuantityDict.update(self.kh2slotdata["ShieldAbilities"])
|
||||
|
||||
all_weapon_location_id = []
|
||||
for weapon_location in all_weapon_slot:
|
||||
@@ -408,13 +388,15 @@ class KH2Context(CommonContext):
|
||||
async def checkWorldLocations(self):
|
||||
try:
|
||||
currentworldint = self.kh2_read_byte(self.Now)
|
||||
await self.send_msgs([{
|
||||
"cmd": "Set", "key": "Slot: " + str(self.slot) + " :CurrentWorld",
|
||||
"default": 0, "want_reply": True, "operations": [{
|
||||
"operation": "replace",
|
||||
"value": currentworldint
|
||||
}]
|
||||
}])
|
||||
if self.last_world_int != currentworldint:
|
||||
self.last_world_int = currentworldint
|
||||
await self.send_msgs([{
|
||||
"cmd": "Set", "key": "Slot: " + str(self.slot) + " :CurrentWorld",
|
||||
"default": 0, "want_reply": False, "operations": [{
|
||||
"operation": "replace",
|
||||
"value": currentworldint
|
||||
}]
|
||||
}])
|
||||
if currentworldint in self.worldid_to_locations:
|
||||
curworldid = self.worldid_to_locations[currentworldint]
|
||||
for location, data in curworldid.items():
|
||||
@@ -525,27 +507,7 @@ class KH2Context(CommonContext):
|
||||
if itemname not in self.kh2_seed_save_cache["AmountInvo"]["Ability"]:
|
||||
self.kh2_seed_save_cache["AmountInvo"]["Ability"][itemname] = []
|
||||
# appending the slot that the ability should be in
|
||||
# for non beta. remove after 4.3
|
||||
if "PoptrackerVersion" in self.kh2slotdata:
|
||||
if self.kh2slotdata["PoptrackerVersionCheck"] < 4.3:
|
||||
if (itemname in self.sora_ability_set
|
||||
and len(self.kh2_seed_save_cache["AmountInvo"]["Ability"][itemname]) < self.item_name_to_data[itemname].quantity) \
|
||||
and self.kh2_seed_save_cache["SoraInvo"][1] > 0x254C:
|
||||
ability_slot = self.kh2_seed_save_cache["SoraInvo"][1]
|
||||
self.kh2_seed_save_cache["AmountInvo"]["Ability"][itemname].append(ability_slot)
|
||||
self.kh2_seed_save_cache["SoraInvo"][1] -= 2
|
||||
elif itemname in self.donald_ability_set:
|
||||
ability_slot = self.kh2_seed_save_cache["DonaldInvo"][1]
|
||||
self.kh2_seed_save_cache["AmountInvo"]["Ability"][itemname].append(ability_slot)
|
||||
self.kh2_seed_save_cache["DonaldInvo"][1] -= 2
|
||||
else:
|
||||
ability_slot = self.kh2_seed_save_cache["GoofyInvo"][1]
|
||||
self.kh2_seed_save_cache["AmountInvo"]["Ability"][itemname].append(ability_slot)
|
||||
self.kh2_seed_save_cache["GoofyInvo"][1] -= 2
|
||||
if ability_slot in self.front_ability_slots:
|
||||
self.front_ability_slots.remove(ability_slot)
|
||||
|
||||
elif len(self.kh2_seed_save_cache["AmountInvo"]["Ability"][itemname]) < \
|
||||
if len(self.kh2_seed_save_cache["AmountInvo"]["Ability"][itemname]) < \
|
||||
self.AbilityQuantityDict[itemname]:
|
||||
if itemname in self.sora_ability_set:
|
||||
ability_slot = self.kh2_seed_save_cache["SoraInvo"][0]
|
||||
@@ -845,7 +807,7 @@ class KH2Context(CommonContext):
|
||||
logger.info("line 840")
|
||||
|
||||
|
||||
def finishedGame(ctx: KH2Context, message):
|
||||
def finishedGame(ctx: KH2Context):
|
||||
if ctx.kh2slotdata['FinalXemnas'] == 1:
|
||||
if not ctx.final_xemnas and ctx.kh2_read_byte(ctx.Save + all_world_locations[LocationName.FinalXemnas].addrObtained) \
|
||||
& 0x1 << all_world_locations[LocationName.FinalXemnas].bitIndex > 0:
|
||||
@@ -877,8 +839,9 @@ def finishedGame(ctx: KH2Context, message):
|
||||
elif ctx.kh2slotdata['Goal'] == 2:
|
||||
# for backwards compat
|
||||
if "hitlist" in ctx.kh2slotdata:
|
||||
locations = ctx.sending
|
||||
for boss in ctx.kh2slotdata["hitlist"]:
|
||||
if boss in message[0]["locations"]:
|
||||
if boss in locations:
|
||||
ctx.hitlist_bounties += 1
|
||||
if ctx.hitlist_bounties >= ctx.kh2slotdata["BountyRequired"] or ctx.kh2_seed_save_cache["AmountInvo"]["Amount"]["Bounty"] >= ctx.kh2slotdata["BountyRequired"]:
|
||||
if ctx.kh2_read_byte(ctx.Save + 0x36B3) < 1:
|
||||
@@ -919,11 +882,12 @@ async def kh2_watcher(ctx: KH2Context):
|
||||
await asyncio.create_task(ctx.verifyChests())
|
||||
await asyncio.create_task(ctx.verifyItems())
|
||||
await asyncio.create_task(ctx.verifyLevel())
|
||||
message = [{"cmd": 'LocationChecks', "locations": ctx.sending}]
|
||||
if finishedGame(ctx, message) and not ctx.kh2_finished_game:
|
||||
if finishedGame(ctx) and not ctx.kh2_finished_game:
|
||||
await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}])
|
||||
ctx.kh2_finished_game = True
|
||||
await ctx.send_msgs(message)
|
||||
if ctx.sending:
|
||||
message = [{"cmd": 'LocationChecks', "locations": ctx.sending}]
|
||||
await ctx.send_msgs(message)
|
||||
elif not ctx.kh2connected and ctx.serverconneced:
|
||||
logger.info("Game Connection lost. waiting 15 seconds until trying to reconnect.")
|
||||
ctx.kh2 = None
|
||||
|
||||
@@ -7,18 +7,21 @@
|
||||
|
||||
<h2 style="text-transform:none";>Required Software:</h2>
|
||||
|
||||
`Kingdom Hearts II Final Mix` from the [Epic Games Store](https://store.epicgames.com/en-US/discover/kingdom-hearts) or [Steam](https://store.steampowered.com/app/2552430/KINGDOM_HEARTS_HD_1525_ReMIX/)
|
||||
Kingdom Hearts II Final Mix from the [Epic Games Store](https://store.epicgames.com/en-US/discover/kingdom-hearts) or [Steam](https://store.steampowered.com/app/2552430/KINGDOM_HEARTS_HD_1525_ReMIX/)
|
||||
|
||||
- Follow this Guide to set up these requirements [KH2Rando.com](https://tommadness.github.io/KH2Randomizer/setup/Panacea-ModLoader/)
|
||||
1. `Version 3.3.0 or greater OpenKH Mod Manager with Panacea`
|
||||
2. `Lua Backend from the OpenKH Mod Manager`
|
||||
3. `Install the mod KH2FM-Mods-Num/GoA-ROM-Edition using OpenKH Mod Manager`
|
||||
1. Version 3.4.0 or greater OpenKH Mod Manager with Panacea
|
||||
2. Lua Backend from the OpenKH Mod Manager
|
||||
3. Install the mod `KH2FM-Mods-Num/GoA-ROM-Edition` using OpenKH Mod Manager
|
||||
- Needed for Archipelago
|
||||
1. [`ArchipelagoKH2Client.exe`](https://github.com/ArchipelagoMW/Archipelago/releases)
|
||||
2. `Install the Archipelago Companion mod from JaredWeakStrike/APCompanion using OpenKH Mod Manager`
|
||||
3. `Install the Archipelago Quality Of Life mod from JaredWeakStrike/AP_QOL using OpenKH Mod Manager`
|
||||
4. `Install the mod from KH2FM-Mods-equations19/auto-save using OpenKH Mod Manager`
|
||||
5. `AP Randomizer Seed`
|
||||
1. [ArchipelagoKH2Client.exe](https://github.com/ArchipelagoMW/Archipelago/releases)
|
||||
2. Install the Archipelago Companion mod from `JaredWeakStrike/APCompanion` using OpenKH Mod Manager
|
||||
3. Install the mod from `KH2FM-Mods-equations19/auto-save` using OpenKH Mod Manager
|
||||
4. Install the mod from `KH2FM-Mods-equations19/KH2-Lua-Library` using OpenKH Mod Manager
|
||||
5. AP Randomizer Seed
|
||||
- Optional Quality of Life Mods for Archipelago
|
||||
1. Optionally Install the Archipelago Quality Of Life mod from `JaredWeakStrike/AP_QOL` using OpenKH Mod Manager
|
||||
2. Optionally Install the Quality Of Life mod from `shananas/BearSkip` using OpenKH Mod Manager
|
||||
|
||||
<h3 style="text-transform:none";>Required: Archipelago Companion Mod</h3>
|
||||
|
||||
@@ -26,15 +29,21 @@ Load this mod just like the <b>GoA ROM</b> you did during the KH2 Rando setup. `
|
||||
Have this mod second-highest priority below the .zip seed.<br>
|
||||
This mod is based upon Num's Garden of Assemblege Mod and requires it to work. Without Num this could not be possible.
|
||||
|
||||
<h3 style="text-transform:none";>Required: Auto Save Mod</h3>
|
||||
<h3 style="text-transform:none";>Required: Auto Save Mod and KH2 Lua Library</h3>
|
||||
|
||||
Load this mod just like the GoA ROM you did during the KH2 Rando setup. `KH2FM-Mods-equations19/auto-save` Location doesn't matter, required in case of crashes. See [Best Practices](en#best-practices) on how to load the auto save
|
||||
Load these mods just like you loaded the GoA ROM mod during the KH2 Rando setup. `KH2FM-Mods-equations19/auto-save` and `KH2FM-Mods-equations19/KH2-Lua-Library` Location doesn't matter, required in case of crashes. See [Best Practices](en#best-practices) on how to load the auto save
|
||||
|
||||
<h3 style="text-transform:none";>Optional QoL Mods: AP QoL and Bear Skip</h3>
|
||||
|
||||
`JaredWeakStrike/AP_QOL` Makes the urns minigames much faster, makes Cavern of Remembrance orbs drop significantly more drive orbs for refilling drive/leveling master form, skips the animation when using the bulky vendor RC, skips carpet escape auto scroller in Agrabah 2, and prevents the wardrobe in the Beasts Castle wardrobe push minigame from waking up while being pushed.
|
||||
|
||||
`shananas/BearSkip` Skips all minigames in 100 Acre Woods except the Spooky Cave minigame since there are chests in Spooky Cave you can only get during the minigame. For Spooky Cave, Pooh is moved to the other side of the invisible wall that prevents you from using his RC to finish the minigame.
|
||||
|
||||
<h3 style="text-transform:none";>Installing A Seed</h3>
|
||||
|
||||
When you generate a game you will see a download link for a KH2 .zip seed on the room page. Download the seed then open OpenKH Mod Manager and click the green plus and `Select and install Mod Archive`.<br>
|
||||
When you generate a game you will see a download link for a KH2 .zip seed on the room page. Download the seed then open OpenKH Mod Manager and click the green plus and "Select and install Mod Archive".<br>
|
||||
Make sure the seed is on the top of the list (Highest Priority)<br>
|
||||
After Installing the seed click `Mod Loader -> Build/Build and Run`. Every slot is a unique mod to install and will be needed be repatched for different slots/rooms.
|
||||
After Installing the seed click "Mod Loader -> Build/Build and Run". Every slot is a unique mod to install and will be needed be repatched for different slots/rooms.
|
||||
|
||||
<h2 style="text-transform:none";>Optional Software:</h2>
|
||||
|
||||
@@ -48,18 +57,21 @@ After Installing the seed click `Mod Loader -> Build/Build and Run`. Every slot
|
||||
|
||||
<h2 style="text-transform:none";>Using the KH2 Client</h2>
|
||||
|
||||
Once you have started the game through OpenKH Mod Manager and are on the title screen run the [ArchipelagoKH2Client.exe](https://github.com/ArchipelagoMW/Archipelago/releases).<br>
|
||||
Start the game through OpenKH Mod Manager. If starting a new run, enter the Garden of Assemblage from a new save. If returning to a run, load the save and enter the Garden of Assemblage. Then run the [ArchipelagoKH2Client.exe](https://github.com/ArchipelagoMW/Archipelago/releases).<br>
|
||||
When you successfully connect to the server the client will automatically hook into the game to send/receive checks. <br>
|
||||
If the client ever loses connection to the game, it will also disconnect from the server and you will need to reconnect.<br>
|
||||
`Make sure the game is open whenever you try to connect the client to the server otherwise it will immediately disconnect you.`<br>
|
||||
|
||||
Make sure the game is open whenever you try to connect the client to the server otherwise it will immediately disconnect you.<br>
|
||||
|
||||
Most checks will be sent to you anywhere outside a load or cutscene.<br>
|
||||
`If you obtain magic, you will need to pause your game to have it show up in your inventory, then enter a new room for it to become properly usable.`
|
||||
|
||||
If you obtain magic, you will need to pause your game to have it show up in your inventory, then enter a new room for it to become properly usable.
|
||||
|
||||
<h2 style="text-transform:none";>KH2 Client should look like this: </h2>
|
||||
|
||||

|
||||
|
||||
Enter `The room's port number` into the top box <b> where the x's are</b> and press "Connect". Follow the prompts there and you should be connected
|
||||
Enter The room's port number into the top box <b> where the x's are</b> and press "Connect". Follow the prompts there and you should be connected
|
||||
|
||||
<h2 style="text-transform:none";>Common Pitfalls</h2>
|
||||
|
||||
@@ -102,7 +114,7 @@ This pack will handle logic, received items, checked locations and autotabbing f
|
||||
- Why is my Client giving me a "Cannot Open Process: " error?
|
||||
- Due to how the client reads kingdom hearts 2 memory some people's computer flags it as a virus. Run the client as admin.
|
||||
- Why is my HP/MP continuously increasing without stopping?
|
||||
- You do not have `JaredWeakStrike/APCompanion` set up correctly. Make sure it is above the `GoA ROM Mod` in the mod manager.
|
||||
- You do not have `JaredWeakStrike/APCompanion` set up correctly. Make sure it is above the GoA ROM Edition Mod in the mod manager.
|
||||
- Why is my HP/MP continuously increasing without stopping when I have the APCompanion Mod?
|
||||
- You have a leftover GOA lua script in your `Documents\KINGDOM HEARTS HD 1.5+2.5 ReMIX\scripts\KH2`.
|
||||
- Why am I missing worlds/portals in the GoA?
|
||||
@@ -110,9 +122,9 @@ This pack will handle logic, received items, checked locations and autotabbing f
|
||||
- Why did I not load into the correct visit?
|
||||
- You need to trigger a cutscene or visit The World That Never Was for it to register that you have received the item.
|
||||
- What versions of Kingdom Hearts 2 are supported?
|
||||
- Currently the `only` supported versions are `Epic Games Version 1.0.0.9_WW` and `Steam Build Version 14716933`.
|
||||
- Currently the only supported versions are Epic Games Version 1.0.0.10_WW and Steam Build Version 15194255.
|
||||
- Why am I getting wallpapered while going into a world for the first time?
|
||||
- Your `Lua Backend` was not configured correctly. Look over the step in the [KH2Rando.com](https://tommadness.github.io/KH2Randomizer/setup/Panacea-ModLoader/) guide.
|
||||
- Your Lua Backend was not configured correctly. Look over the step in the [KH2Rando.com](https://tommadness.github.io/KH2Randomizer/setup/Panacea-ModLoader/) guide.
|
||||
- Why am I not getting magic?
|
||||
- If you obtain magic, you will need to pause your game to have it show up in your inventory, then enter a new room for it to become properly usable.
|
||||
- Why did I crash after picking my dream weapon?
|
||||
@@ -124,7 +136,7 @@ This pack will handle logic, received items, checked locations and autotabbing f
|
||||
- You will need to get the `JaredWeakStrike/APCompanion` (you can find how to get this if you scroll up)
|
||||
- Why am I not sending or receiving items?
|
||||
- Make sure you are connected to the KH2 client and the correct room (for more information scroll up)
|
||||
- Why should I install the auto save mod at `KH2FM-Mods-equations19/auto-save`?
|
||||
- Because Kingdom Hearts 2 is prone to crashes and will keep you from losing your progress.
|
||||
- Why should I install the auto save mod at `KH2FM-Mods-equations19/auto-save` and `KH2FM-Mods-equations19/KH2-Lua-Library`?
|
||||
- Because Kingdom Hearts 2 is prone to crashes and will keep you from losing your progress. Both mods are needed for auto save to work.
|
||||
- How do I load an auto save?
|
||||
- To load an auto-save, hold down the Select or your equivalent on your prefered controller while choosing a file. Make sure to hold the button down the whole time.
|
||||
|
||||
@@ -103,6 +103,7 @@ def generateRom(args, world: "LinksAwakeningWorld"):
|
||||
assembler.const("wGoldenLeaves", 0xDB42) # New memory location where to store the golden leaf counter
|
||||
assembler.const("wCollectedTunics", 0xDB6D) # Memory location where to store which tunic options are available (and boots)
|
||||
assembler.const("wCustomMessage", 0xC0A0)
|
||||
assembler.const("wOverworldRoomStatus", 0xD800)
|
||||
|
||||
# We store the link info in unused color dungeon flags, so it gets preserved in the savegame.
|
||||
assembler.const("wLinkSyncSequenceNumber", 0xDDF6)
|
||||
|
||||
@@ -68,10 +68,12 @@ DEFAULT_ITEM_POOL = {
|
||||
|
||||
|
||||
class ItemPool:
|
||||
def __init__(self, logic, settings, rnd):
|
||||
def __init__(self, logic, settings, rnd, stabilize_item_pool: bool):
|
||||
self.__pool = {}
|
||||
self.__setup(logic, settings)
|
||||
self.__randomizeRupees(settings, rnd)
|
||||
|
||||
if not stabilize_item_pool:
|
||||
self.__randomizeRupees(settings, rnd)
|
||||
|
||||
def add(self, item, count=1):
|
||||
self.__pool[item] = self.__pool.get(item, 0) + count
|
||||
|
||||
@@ -2,6 +2,10 @@ import typing
|
||||
from ..checkMetadata import checkMetadataTable
|
||||
from .constants import *
|
||||
|
||||
custom_name_replacements = {
|
||||
'"':"'",
|
||||
'_':' ',
|
||||
}
|
||||
|
||||
class ItemInfo:
|
||||
MULTIWORLD = True
|
||||
@@ -23,6 +27,11 @@ class ItemInfo:
|
||||
def setLocation(self, location):
|
||||
self._location = location
|
||||
|
||||
def setCustomItemName(self, name):
|
||||
for key, val in custom_name_replacements.items():
|
||||
name = name.replace(key, val)
|
||||
self.custom_item_name = name
|
||||
|
||||
def getOptions(self):
|
||||
return self.OPTIONS
|
||||
|
||||
|
||||
@@ -716,9 +716,7 @@ def addWarpImprovements(rom, extra_warps):
|
||||
|
||||
# Allow cursor to move over black squares
|
||||
# This allows warping to undiscovered areas - a fine cheat, but needs a check for wOverworldRoomStatus in the warp code
|
||||
CHEAT_WARP_ANYWHERE = False
|
||||
if CHEAT_WARP_ANYWHERE:
|
||||
rom.patch(0x01, 0x1AE8, None, ASM("jp $5AF5"))
|
||||
rom.patch(0x01, 0x1AE8, None, ASM("jp $5AF5"))
|
||||
|
||||
# This disables the arrows around the selection bubble
|
||||
#rom.patch(0x01, 0x1B6F, None, ASM("ret"), fill_nop=True)
|
||||
@@ -797,8 +795,14 @@ def addWarpImprovements(rom, extra_warps):
|
||||
TeleportHandler:
|
||||
|
||||
ld a, [$DBB4] ; Load the current selected tile
|
||||
; TODO: check if actually revealed so we can have free movement
|
||||
; Check cursor against different tiles to see if we are selecting a warp
|
||||
ld hl, wOverworldRoomStatus
|
||||
ld e, a ; $5D38: $5F
|
||||
ld d, $00 ; $5D39: $16 $00
|
||||
add hl, de ; $5D3B: $19
|
||||
ld a, [hl]
|
||||
and $80
|
||||
jr z, exit
|
||||
ld a, [$DBB4] ; Load the current selected tile
|
||||
{warp_jump}
|
||||
jr exit
|
||||
|
||||
|
||||
@@ -527,6 +527,13 @@ class InGameHints(DefaultOnToggle):
|
||||
display_name = "In-game Hints"
|
||||
|
||||
|
||||
class StabilizeItemPool(DefaultOffToggle):
|
||||
"""
|
||||
By default, rupees in the item pool may be randomly swapped with bombs, arrows, powders, or capacity upgrades. This option disables that swapping, which is useful for plando.
|
||||
"""
|
||||
display_name = "Stabilize Item Pool"
|
||||
|
||||
|
||||
class ForeignItemIcons(Choice):
|
||||
"""
|
||||
Choose how to display foreign items.
|
||||
@@ -562,6 +569,7 @@ ladx_option_groups = [
|
||||
TrendyGame,
|
||||
InGameHints,
|
||||
NagMessages,
|
||||
StabilizeItemPool,
|
||||
Quickswap,
|
||||
HardMode,
|
||||
BootsControls
|
||||
@@ -631,6 +639,7 @@ class LinksAwakeningOptions(PerGameCommonOptions):
|
||||
no_flash: NoFlash
|
||||
in_game_hints: InGameHints
|
||||
overworld: Overworld
|
||||
stabilize_item_pool: StabilizeItemPool
|
||||
|
||||
warp_improvements: Removed
|
||||
additional_warp_points: Removed
|
||||
|
||||
@@ -138,7 +138,30 @@ class LinksAwakeningWorld(World):
|
||||
world_setup = LADXRWorldSetup()
|
||||
world_setup.randomize(self.ladxr_settings, self.random)
|
||||
self.ladxr_logic = LADXRLogic(configuration_options=self.ladxr_settings, world_setup=world_setup)
|
||||
self.ladxr_itempool = LADXRItemPool(self.ladxr_logic, self.ladxr_settings, self.random).toDict()
|
||||
self.ladxr_itempool = LADXRItemPool(self.ladxr_logic, self.ladxr_settings, self.random, bool(self.options.stabilize_item_pool)).toDict()
|
||||
|
||||
|
||||
def generate_early(self) -> None:
|
||||
self.dungeon_item_types = {
|
||||
}
|
||||
for dungeon_item_type in ["maps", "compasses", "small_keys", "nightmare_keys", "stone_beaks", "instruments"]:
|
||||
option_name = "shuffle_" + dungeon_item_type
|
||||
option: DungeonItemShuffle = getattr(self.options, option_name)
|
||||
|
||||
self.dungeon_item_types[option.ladxr_item] = option.value
|
||||
|
||||
# The color dungeon does not contain an instrument
|
||||
num_items = 8 if dungeon_item_type == "instruments" else 9
|
||||
|
||||
# For any and different world, set item rule instead
|
||||
if option.value == DungeonItemShuffle.option_own_world:
|
||||
self.options.local_items.value |= {
|
||||
ladxr_item_to_la_item_name[f"{option.ladxr_item}{i}"] for i in range(1, num_items + 1)
|
||||
}
|
||||
elif option.value == DungeonItemShuffle.option_different_world:
|
||||
self.options.non_local_items.value |= {
|
||||
ladxr_item_to_la_item_name[f"{option.ladxr_item}{i}"] for i in range(1, num_items + 1)
|
||||
}
|
||||
|
||||
def create_regions(self) -> None:
|
||||
# Initialize
|
||||
@@ -185,32 +208,9 @@ class LinksAwakeningWorld(World):
|
||||
def create_items(self) -> None:
|
||||
exclude = [item.name for item in self.multiworld.precollected_items[self.player]]
|
||||
|
||||
dungeon_item_types = {
|
||||
|
||||
}
|
||||
|
||||
self.prefill_original_dungeon = [ [], [], [], [], [], [], [], [], [] ]
|
||||
self.prefill_own_dungeons = []
|
||||
self.pre_fill_items = []
|
||||
# For any and different world, set item rule instead
|
||||
|
||||
for dungeon_item_type in ["maps", "compasses", "small_keys", "nightmare_keys", "stone_beaks", "instruments"]:
|
||||
option_name = "shuffle_" + dungeon_item_type
|
||||
option: DungeonItemShuffle = getattr(self.options, option_name)
|
||||
|
||||
dungeon_item_types[option.ladxr_item] = option.value
|
||||
|
||||
# The color dungeon does not contain an instrument
|
||||
num_items = 8 if dungeon_item_type == "instruments" else 9
|
||||
|
||||
if option.value == DungeonItemShuffle.option_own_world:
|
||||
self.options.local_items.value |= {
|
||||
ladxr_item_to_la_item_name[f"{option.ladxr_item}{i}"] for i in range(1, num_items + 1)
|
||||
}
|
||||
elif option.value == DungeonItemShuffle.option_different_world:
|
||||
self.options.non_local_items.value |= {
|
||||
ladxr_item_to_la_item_name[f"{option.ladxr_item}{i}"] for i in range(1, num_items + 1)
|
||||
}
|
||||
# option_original_dungeon = 0
|
||||
# option_own_dungeons = 1
|
||||
# option_own_world = 2
|
||||
@@ -226,7 +226,7 @@ class LinksAwakeningWorld(World):
|
||||
for _ in range(count):
|
||||
if item_name in exclude:
|
||||
exclude.remove(item_name) # this is destructive. create unique list above
|
||||
self.multiworld.itempool.append(self.create_item("Nothing"))
|
||||
self.multiworld.itempool.append(self.create_item(self.get_filler_item_name()))
|
||||
else:
|
||||
item = self.create_item(item_name)
|
||||
|
||||
@@ -238,7 +238,7 @@ class LinksAwakeningWorld(World):
|
||||
|
||||
if isinstance(item.item_data, DungeonItemData):
|
||||
item_type = item.item_data.ladxr_id[:-1]
|
||||
shuffle_type = dungeon_item_types[item_type]
|
||||
shuffle_type = self.dungeon_item_types[item_type]
|
||||
|
||||
if item.item_data.dungeon_item_type == DungeonItemType.INSTRUMENT and shuffle_type == ShuffleInstruments.option_vanilla:
|
||||
# Find instrument, lock
|
||||
@@ -439,7 +439,7 @@ class LinksAwakeningWorld(World):
|
||||
# Otherwise, use a cute letter as the icon
|
||||
elif self.options.foreign_item_icons == 'guess_by_name':
|
||||
loc.ladxr_item.item = self.guess_icon_for_other_world(loc.item)
|
||||
loc.ladxr_item.custom_item_name = loc.item.name
|
||||
loc.ladxr_item.setCustomItemName(loc.item.name)
|
||||
|
||||
else:
|
||||
if loc.item.advancement:
|
||||
@@ -500,8 +500,14 @@ class LinksAwakeningWorld(World):
|
||||
state.prog_items[self.player]["RUPEES"] -= self.rupees[item.name]
|
||||
return change
|
||||
|
||||
# Same fill choices and weights used in LADXR.itempool.__randomizeRupees
|
||||
filler_choices = ("Bomb", "Single Arrow", "10 Arrows", "Magic Powder", "Medicine")
|
||||
filler_weights = ( 10, 5, 10, 10, 1)
|
||||
|
||||
def get_filler_item_name(self) -> str:
|
||||
return "Nothing"
|
||||
if self.options.stabilize_item_pool:
|
||||
return "Nothing"
|
||||
return self.random.choices(self.filler_choices, self.filler_weights)[0]
|
||||
|
||||
def fill_slot_data(self):
|
||||
slot_data = {}
|
||||
|
||||
@@ -128,6 +128,9 @@ class LingoWorld(World):
|
||||
pool.append(self.create_item("Puzzle Skip"))
|
||||
|
||||
if traps:
|
||||
if self.options.speed_boost_mode:
|
||||
self.options.trap_weights.value["Slowness Trap"] = 0
|
||||
|
||||
total_weight = sum(self.options.trap_weights.values())
|
||||
|
||||
if total_weight == 0:
|
||||
@@ -171,7 +174,7 @@ class LingoWorld(World):
|
||||
"death_link", "victory_condition", "shuffle_colors", "shuffle_doors", "shuffle_paintings", "shuffle_panels",
|
||||
"enable_pilgrimage", "sunwarp_access", "mastery_achievements", "level_2_requirement", "location_checks",
|
||||
"early_color_hallways", "pilgrimage_allows_roof_access", "pilgrimage_allows_paintings", "shuffle_sunwarps",
|
||||
"group_doors"
|
||||
"group_doors", "speed_boost_mode"
|
||||
]
|
||||
|
||||
slot_data = {
|
||||
@@ -188,5 +191,8 @@ class LingoWorld(World):
|
||||
return slot_data
|
||||
|
||||
def get_filler_item_name(self) -> str:
|
||||
filler_list = [":)", "The Feeling of Being Lost", "Wanderlust", "Empty White Hallways"]
|
||||
return self.random.choice(filler_list)
|
||||
if self.options.speed_boost_mode:
|
||||
return "Speed Boost"
|
||||
else:
|
||||
filler_list = [":)", "The Feeling of Being Lost", "Wanderlust", "Empty White Hallways"]
|
||||
return self.random.choice(filler_list)
|
||||
|
||||
Binary file not shown.
@@ -17,6 +17,7 @@ special_items:
|
||||
Iceland Trap: 444411
|
||||
Atbash Trap: 444412
|
||||
Puzzle Skip: 444413
|
||||
Speed Boost: 444680
|
||||
panels:
|
||||
Starting Room:
|
||||
HI: 444400
|
||||
|
||||
@@ -85,6 +85,7 @@ def load_item_data():
|
||||
"The Feeling of Being Lost": ItemClassification.filler,
|
||||
"Wanderlust": ItemClassification.filler,
|
||||
"Empty White Hallways": ItemClassification.filler,
|
||||
"Speed Boost": ItemClassification.filler,
|
||||
**{trap_name: ItemClassification.trap for trap_name in TRAP_ITEMS},
|
||||
"Puzzle Skip": ItemClassification.useful,
|
||||
}
|
||||
|
||||
@@ -232,6 +232,14 @@ class TrapWeights(OptionDict):
|
||||
default = {trap_name: 1 for trap_name in TRAP_ITEMS}
|
||||
|
||||
|
||||
class SpeedBoostMode(Toggle):
|
||||
"""
|
||||
If on, the player's default speed is halved, as if affected by a Slowness Trap. Speed Boosts are added to
|
||||
the item pool, which temporarily return the player to normal speed. Slowness Traps are removed from the pool.
|
||||
"""
|
||||
display_name = "Speed Boost Mode"
|
||||
|
||||
|
||||
class PuzzleSkipPercentage(Range):
|
||||
"""Replaces junk items with puzzle skips, at the specified rate."""
|
||||
display_name = "Puzzle Skip Percentage"
|
||||
@@ -260,6 +268,7 @@ lingo_option_groups = [
|
||||
Level2Requirement,
|
||||
TrapPercentage,
|
||||
TrapWeights,
|
||||
SpeedBoostMode,
|
||||
PuzzleSkipPercentage,
|
||||
])
|
||||
]
|
||||
@@ -287,6 +296,7 @@ class LingoOptions(PerGameCommonOptions):
|
||||
shuffle_postgame: ShufflePostgame
|
||||
trap_percentage: TrapPercentage
|
||||
trap_weights: TrapWeights
|
||||
speed_boost_mode: SpeedBoostMode
|
||||
puzzle_skip_percentage: PuzzleSkipPercentage
|
||||
death_link: DeathLink
|
||||
start_inventory_from_pool: StartInventoryPool
|
||||
|
||||
@@ -59,4 +59,11 @@ class TestShuffleSunwarpsAccess(LingoTestBase):
|
||||
"victory_condition": "pilgrimage",
|
||||
"shuffle_sunwarps": "true",
|
||||
"sunwarp_access": "individual"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class TestSpeedBoostMode(LingoTestBase):
|
||||
options = {
|
||||
"location_checks": "insanity",
|
||||
"speed_boost_mode": "true",
|
||||
}
|
||||
|
||||
@@ -216,3 +216,6 @@ config.each do |room_name, room_data|
|
||||
end
|
||||
|
||||
File.write(outputpath, old_generated.to_yaml)
|
||||
|
||||
puts "Next item ID: #{next_item_id}"
|
||||
puts "Next location ID: #{next_location_id}"
|
||||
|
||||
@@ -381,7 +381,7 @@ class MessengerWorld(World):
|
||||
return
|
||||
# the messenger client calls into AP with specific args, so check the out path matches what the client sends
|
||||
out_path = output_path(multiworld.get_out_file_name_base(1) + ".aptm")
|
||||
if "The Messenger\\Archipelago\\output" not in out_path:
|
||||
if "Messenger\\Archipelago\\output" not in out_path:
|
||||
return
|
||||
import orjson
|
||||
data = {
|
||||
|
||||
@@ -214,10 +214,19 @@ class MegaMan2Client(BizHawkClient):
|
||||
last_wily: Optional[int] = None # default to wily 1
|
||||
|
||||
async def validate_rom(self, ctx: "BizHawkClientContext") -> bool:
|
||||
from worlds._bizhawk import RequestFailedError, read
|
||||
from worlds._bizhawk import RequestFailedError, read, get_memory_size
|
||||
from . import MM2World
|
||||
|
||||
try:
|
||||
if (await get_memory_size(ctx.bizhawk_ctx, "PRG ROM")) < 0x3FFB0:
|
||||
if "pool" in ctx.command_processor.commands:
|
||||
ctx.command_processor.commands.pop("pool")
|
||||
if "request" in ctx.command_processor.commands:
|
||||
ctx.command_processor.commands.pop("request")
|
||||
if "autoheal" in ctx.command_processor.commands:
|
||||
ctx.command_processor.commands.pop("autoheal")
|
||||
return False
|
||||
|
||||
game_name, version = (await read(ctx.bizhawk_ctx, [(0x3FFB0, 21, "PRG ROM"),
|
||||
(0x3FFC8, 3, "PRG ROM")]))
|
||||
if game_name[:3] != b"MM2" or version != bytes(MM2World.world_version):
|
||||
|
||||
@@ -85,7 +85,7 @@ keyItemList: typing.List[ItemData] = [
|
||||
]
|
||||
|
||||
subChipList: typing.List[ItemData] = [
|
||||
ItemData(0xB31018, ItemName.Unlocker, ItemClassification.useful, ItemType.SubChip, 117),
|
||||
ItemData(0xB31018, ItemName.Unlocker, ItemClassification.progression, ItemType.SubChip, 117),
|
||||
ItemData(0xB31019, ItemName.Untrap, ItemClassification.filler, ItemType.SubChip, 115),
|
||||
ItemData(0xB3101A, ItemName.LockEnmy, ItemClassification.filler, ItemType.SubChip, 116),
|
||||
ItemData(0xB3101B, ItemName.MiniEnrg, ItemClassification.filler, ItemType.SubChip, 112),
|
||||
@@ -290,7 +290,9 @@ programList: typing.List[ItemData] = [
|
||||
ItemData(0xB31099, ItemName.WpnLV_plus_Yellow, ItemClassification.filler, ItemType.Program, 35, ProgramColor.Yellow),
|
||||
ItemData(0xB3109A, ItemName.Press, ItemClassification.progression, ItemType.Program, 20, ProgramColor.White),
|
||||
|
||||
ItemData(0xB310B7, ItemName.UnderSht, ItemClassification.useful, ItemType.Program, 30, ProgramColor.White)
|
||||
ItemData(0xB310B7, ItemName.UnderSht, ItemClassification.useful, ItemType.Program, 30, ProgramColor.White),
|
||||
ItemData(0xB310E0, ItemName.Humor, ItemClassification.progression, ItemType.Program, 45, ProgramColor.Pink),
|
||||
ItemData(0xB310E1, ItemName.BlckMnd, ItemClassification.progression, ItemType.Program, 46, ProgramColor.White)
|
||||
]
|
||||
|
||||
zennyList: typing.List[ItemData] = [
|
||||
@@ -338,8 +340,29 @@ item_frequencies: typing.Dict[str, int] = {
|
||||
ItemName.zenny_800z: 2,
|
||||
ItemName.zenny_1000z: 2,
|
||||
ItemName.zenny_1200z: 2,
|
||||
ItemName.bugfrag_01: 5,
|
||||
ItemName.bugfrag_01: 10,
|
||||
ItemName.bugfrag_10: 5
|
||||
}
|
||||
|
||||
item_groups: typing.Dict[str, typing.Set[str]] = {
|
||||
"Key Items": {loc.itemName for loc in keyItemList},
|
||||
"Subchips": {loc.itemName for loc in subChipList},
|
||||
"Programs": {loc.itemName for loc in programList},
|
||||
"BattleChips": {loc.itemName for loc in chipList},
|
||||
"Zenny": {loc.itemName for loc in zennyList},
|
||||
"BugFrags": {loc.itemName for loc in bugFragList},
|
||||
"Navi Chips": {
|
||||
ItemName.Roll_R, ItemName.RollV2_R, ItemName.RollV3_R, ItemName.GutsMan_G, ItemName.GutsManV2_G,
|
||||
ItemName.GutsManV3_G, ItemName.ProtoMan_B, ItemName.ProtoManV2_B, ItemName.ProtoManV3_B, ItemName.FlashMan_F,
|
||||
ItemName.FlashManV2_F, ItemName.FlashManV3_F, ItemName.BeastMan_B, ItemName.BeastManV2_B, ItemName.BeastManV3_B,
|
||||
ItemName.BubblMan_B, ItemName.BubblManV2_B, ItemName.BubblManV3_B, ItemName.DesertMan_D, ItemName.DesertManV2_D,
|
||||
ItemName.DesertManV3_D, ItemName.PlantMan_P, ItemName.PlantManV2_P, ItemName.PlantManV3_P, ItemName.FlamMan_F,
|
||||
ItemName.FlamManV2_F, ItemName.FlamManV3_F, ItemName.DrillMan_D, ItemName.DrillManV2_D, ItemName.DrillManV3_D,
|
||||
ItemName.MetalMan_M, ItemName.MetalManV2_M, ItemName.MetalManV3_M, ItemName.KingMan_K, ItemName.KingManV2_K,
|
||||
ItemName.KingManV3_K, ItemName.BowlMan_B, ItemName.BowlManV2_B, ItemName.BowlManV3_B
|
||||
}
|
||||
}
|
||||
|
||||
all_items: typing.List[ItemData] = keyItemList + subChipList + chipList + programList + zennyList + bugFragList
|
||||
item_table: typing.Dict[str, ItemData] = {item.itemName: item for item in all_items}
|
||||
items_by_id: typing.Dict[int, ItemData] = {item.code: item for item in all_items}
|
||||
|
||||
@@ -221,7 +221,8 @@ overworlds = [
|
||||
LocationData(LocationName.Hades_Boat_Dock, 0xb310ab, 0x200024c, 0x10, 0x7519B0, 223, [3]),
|
||||
LocationData(LocationName.WWW_Control_Room_1_Screen, 0xb310ac, 0x200024d, 0x40, 0x7596C4, 222, [3, 4]),
|
||||
LocationData(LocationName.WWW_Wilys_Desk, 0xb310ad, 0x200024d, 0x2, 0x759384, 229, [3]),
|
||||
LocationData(LocationName.Undernet_4_Pillar_Prog, 0xb310ae, 0x2000161, 0x1, 0x7746C8, 191, [0, 1])
|
||||
LocationData(LocationName.Undernet_4_Pillar_Prog, 0xb310ae, 0x2000161, 0x1, 0x7746C8, 191, [0, 1]),
|
||||
LocationData(LocationName.Serenade, 0xb3110f, 0x2000178, 0x40, 0x7B3C74, 1, [0])
|
||||
]
|
||||
|
||||
jobs = [
|
||||
@@ -240,7 +241,8 @@ jobs = [
|
||||
# LocationData(LocationName.Gathering_Data, 0xb310bb, 0x2000300, 0x10, 0x739580, 193, [0]),
|
||||
LocationData(LocationName.Somebody_please_help, 0xb310bc, 0x2000301, 0x4, 0x73A14C, 193, [0]),
|
||||
LocationData(LocationName.Looking_for_condor, 0xb310bd, 0x2000301, 0x2, 0x749444, 203, [0]),
|
||||
LocationData(LocationName.Help_with_rehab, 0xb310be, 0x2000301, 0x1, 0x762CF0, 192, [3]),
|
||||
LocationData(LocationName.Help_with_rehab, 0xb310be, 0x2000301, 0x1, 0x762CF0, 192, [0]),
|
||||
LocationData(LocationName.Help_with_rehab_bonus, 0xb3110e, 0x2000301, 0x1, 0x762CF0, 192, [3]),
|
||||
LocationData(LocationName.Old_Master, 0xb310bf, 0x2000302, 0x80, 0x760E80, 193, [0]),
|
||||
LocationData(LocationName.Catching_gang_members, 0xb310c0, 0x2000302, 0x40, 0x76EAE4, 193, [0]),
|
||||
LocationData(LocationName.Please_adopt_a_virus, 0xb310c1, 0x2000302, 0x20, 0x76A4F4, 193, [0]),
|
||||
@@ -250,7 +252,7 @@ jobs = [
|
||||
LocationData(LocationName.Hide_and_seek_Second_Child, 0xb310c5, 0x2000188, 0x2, 0x75ADA8, 191, [0]),
|
||||
LocationData(LocationName.Hide_and_seek_Third_Child, 0xb310c6, 0x2000188, 0x1, 0x75B5EC, 191, [0]),
|
||||
LocationData(LocationName.Hide_and_seek_Fourth_Child, 0xb310c7, 0x2000189, 0x80, 0x75BEB0, 191, [0]),
|
||||
LocationData(LocationName.Hide_and_seek_Completion, 0xb310c8, 0x2000302, 0x8, 0x7406A0, 193, [0]),
|
||||
LocationData(LocationName.Hide_and_seek_Completion, 0xb310c8, 0x2000302, 0x8, 0x742D40, 193, [0]),
|
||||
LocationData(LocationName.Finding_the_blue_Navi, 0xb310c9, 0x2000302, 0x4, 0x773700, 192, [0]),
|
||||
LocationData(LocationName.Give_your_support, 0xb310ca, 0x2000302, 0x2, 0x752D80, 192, [0]),
|
||||
LocationData(LocationName.Stamp_collecting, 0xb310cb, 0x2000302, 0x1, 0x756074, 193, [0]),
|
||||
@@ -329,10 +331,7 @@ chocolate_shop = [
|
||||
LocationData(LocationName.Chocolate_Shop_32, 0xb3110d, 0x20001c3, 0x01, 0x73F8FC, 181, [0]),
|
||||
]
|
||||
|
||||
always_excluded_locations = [
|
||||
LocationName.Undernet_7_PMD,
|
||||
LocationName.Undernet_7_Northeast_BMD,
|
||||
LocationName.Undernet_7_Northwest_BMD,
|
||||
secret_locations = {
|
||||
LocationName.Secret_1_Northwest_BMD,
|
||||
LocationName.Secret_1_Northeast_BMD,
|
||||
LocationName.Secret_1_South_BMD,
|
||||
@@ -341,19 +340,23 @@ always_excluded_locations = [
|
||||
LocationName.Secret_2_Island_BMD,
|
||||
LocationName.Secret_3_Island_BMD,
|
||||
LocationName.Secret_3_BugFrag_BMD,
|
||||
LocationName.Secret_3_South_BMD
|
||||
]
|
||||
LocationName.Secret_3_South_BMD,
|
||||
LocationName.Serenade
|
||||
}
|
||||
|
||||
location_groups: typing.Dict[str, typing.Set[str]] = {
|
||||
"BMDs": {loc.name for loc in bmds},
|
||||
"PMDs": {loc.name for loc in pmds},
|
||||
"Jobs": {loc.name for loc in jobs},
|
||||
"Number Trader": {loc.name for loc in number_traders},
|
||||
"Bugfrag Trader": {loc.name for loc in chocolate_shop},
|
||||
"Secret Area": {LocationName.Secret_1_Northwest_BMD, LocationName.Secret_1_Northeast_BMD,
|
||||
LocationName.Secret_1_South_BMD, LocationName.Secret_2_Upper_BMD, LocationName.Secret_2_Lower_BMD,
|
||||
LocationName.Secret_2_Island_BMD, LocationName.Secret_3_Island_BMD,
|
||||
LocationName.Secret_3_BugFrag_BMD, LocationName.Secret_3_South_BMD, LocationName.Serenade},
|
||||
}
|
||||
|
||||
all_locations: typing.List[LocationData] = bmds + pmds + overworlds + jobs + number_traders + chocolate_shop
|
||||
scoutable_locations: typing.List[LocationData] = [loc for loc in all_locations if loc.hint_flag is not None]
|
||||
location_table: typing.Dict[str, int] = {locData.name: locData.id for locData in all_locations}
|
||||
location_data_table: typing.Dict[str, LocationData] = {locData.name: locData for locData in all_locations}
|
||||
|
||||
|
||||
"""
|
||||
def setup_locations(world, player: int):
|
||||
# If we later include options to change what gets added to the random pool,
|
||||
# this is where they would be changed
|
||||
return {locData.name: locData.id for locData in all_locations}
|
||||
"""
|
||||
|
||||
@@ -173,6 +173,8 @@ class ItemName():
|
||||
WpnLV_plus_White = "WpnLV+1 (White)"
|
||||
Press = "Press"
|
||||
UnderSht = "UnderSht"
|
||||
Humor = "Humor"
|
||||
BlckMnd = "BlckMnd"
|
||||
|
||||
## Currency
|
||||
zenny_200z = "200z"
|
||||
|
||||
@@ -210,6 +210,7 @@ class LocationName():
|
||||
WWW_Control_Room_1_Screen = "WWW Control Room 1 Screen"
|
||||
WWW_Wilys_Desk = "WWW Wily's Desk"
|
||||
Undernet_4_Pillar_Prog = "Undernet 4 Pillar Prog"
|
||||
Serenade = "Serenade"
|
||||
|
||||
## Numberman Codes
|
||||
Numberman_Code_01 = "Numberman Code 01"
|
||||
@@ -261,6 +262,7 @@ class LocationName():
|
||||
Somebody_please_help = "Job: Somebody, please help!"
|
||||
Looking_for_condor = "Job: Looking for condor"
|
||||
Help_with_rehab = "Job: Help with rehab"
|
||||
Help_with_rehab_bonus = "Job: Help with rehab bonus"
|
||||
Old_Master = "Job: Old Master"
|
||||
Catching_gang_members = "Job: Catching gang members"
|
||||
Please_adopt_a_virus = "Job: Please adopt a virus!"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from dataclasses import dataclass
|
||||
from Options import Choice, Range, DefaultOnToggle, PerGameCommonOptions
|
||||
from Options import Choice, Range, DefaultOnToggle, Toggle, PerGameCommonOptions
|
||||
|
||||
|
||||
class ExtraRanks(Range):
|
||||
@@ -17,10 +17,17 @@ class ExtraRanks(Range):
|
||||
|
||||
class IncludeJobs(DefaultOnToggle):
|
||||
"""
|
||||
Whether Jobs can be included in logic.
|
||||
Whether Jobs can contain progression or useful items.
|
||||
"""
|
||||
display_name = "Include Jobs"
|
||||
|
||||
|
||||
class IncludeSecretArea(Toggle):
|
||||
"""
|
||||
Whether the Secret Area (including Serenade) can contain progression or useful items.
|
||||
"""
|
||||
display_name = "Include Secret Area"
|
||||
|
||||
# Possible logic options:
|
||||
# - Include Number Trader
|
||||
# - Include Secret Area
|
||||
@@ -46,5 +53,6 @@ class TradeQuestHinting(Choice):
|
||||
class MMBN3Options(PerGameCommonOptions):
|
||||
extra_ranks: ExtraRanks
|
||||
include_jobs: IncludeJobs
|
||||
include_secret: IncludeSecretArea
|
||||
trade_quest_hinting: TradeQuestHinting
|
||||
|
||||
@@ -135,6 +135,7 @@ regions = [
|
||||
LocationName.Somebody_please_help,
|
||||
LocationName.Looking_for_condor,
|
||||
LocationName.Help_with_rehab,
|
||||
LocationName.Help_with_rehab_bonus,
|
||||
LocationName.Old_Master,
|
||||
LocationName.Catching_gang_members,
|
||||
LocationName.Please_adopt_a_virus,
|
||||
@@ -349,6 +350,7 @@ regions = [
|
||||
LocationName.Secret_2_Upper_BMD,
|
||||
LocationName.Secret_3_Island_BMD,
|
||||
LocationName.Secret_3_South_BMD,
|
||||
LocationName.Secret_3_BugFrag_BMD
|
||||
LocationName.Secret_3_BugFrag_BMD,
|
||||
LocationName.Serenade
|
||||
])
|
||||
]
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user