mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-03-26 17:23:23 -07:00
Compare commits
110 Commits
NewSoupVi-
...
revert-404
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
83d8bd584b | ||
|
|
2751ccdaab | ||
|
|
6287bc27a6 | ||
|
|
97f2c25924 | ||
|
|
e5a0ef799f | ||
|
|
216e0603e1 | ||
|
|
05a67386c6 | ||
|
|
0ec9039ca6 | ||
|
|
f06f95d03d | ||
|
|
5a853dfccd | ||
|
|
23469fa5c3 | ||
|
|
dc1da4e88b | ||
|
|
67f6b458d7 | ||
|
|
8193fa12b2 | ||
|
|
de0c498470 | ||
|
|
7337309426 | ||
|
|
3205e9b3a0 | ||
|
|
05439012dc | ||
|
|
177c0fef52 | ||
|
|
5c4e81d046 | ||
|
|
a2d585ba5c | ||
|
|
5ea55d77b0 | ||
|
|
ab8caea8be | ||
|
|
a043ed50a6 | ||
|
|
e85a835b47 | ||
|
|
9a9fea0ca2 | ||
|
|
e910a37273 | ||
|
|
f06d4503d8 | ||
|
|
8021b457b6 | ||
|
|
d43dc62485 | ||
|
|
f7ec3d7508 | ||
|
|
99c02a3eb3 | ||
|
|
449782a4d8 | ||
|
|
97ca2ad258 | ||
|
|
2b88be5791 | ||
|
|
204e940f47 | ||
|
|
69d3db21df | ||
|
|
41ddb96b24 | ||
|
|
ba8f03516e | ||
|
|
0095eecf2b | ||
|
|
79942c09c2 | ||
|
|
1b15c6920d | ||
|
|
499d79f089 | ||
|
|
926e08513c | ||
|
|
025c550991 | ||
|
|
fced9050a4 | ||
|
|
2ee8b7535d | ||
|
|
0d35cd4679 | ||
|
|
db5d9fbf70 | ||
|
|
51a6dc150c | ||
|
|
710609fa60 | ||
|
|
da781bb4ac | ||
|
|
69487661dd | ||
|
|
f73c0d9894 | ||
|
|
6fac83b84c | ||
|
|
debb936618 | ||
|
|
8c5b65ff26 | ||
|
|
a7c96436d9 | ||
|
|
4e60f3cc54 | ||
|
|
30a0b337a2 | ||
|
|
4ea1dddd2f | ||
|
|
dc218b7997 | ||
|
|
78c5489189 | ||
|
|
d1a7bc66e6 | ||
|
|
b982e9ebb4 | ||
|
|
8f7e0dc441 | ||
|
|
5aea8d4ab5 | ||
|
|
97be5f1dde | ||
|
|
dae3fe188d | ||
|
|
96542fb2d8 | ||
|
|
ec50b0716a | ||
|
|
f8d3c26e3c | ||
|
|
1c0cec0de2 | ||
|
|
4692e6f08a | ||
|
|
b8d23ec595 | ||
|
|
ce42e42af7 | ||
|
|
ee12dda361 | ||
|
|
84805a4e54 | ||
|
|
5530d181da | ||
|
|
ed948e3e5b | ||
|
|
7621889b8b | ||
|
|
c9f1a21bd2 | ||
|
|
874392756b | ||
|
|
7ff201e32c | ||
|
|
170aedba8f | ||
|
|
09c7f5f909 | ||
|
|
4aab317665 | ||
|
|
e52ce0149a | ||
|
|
5a5162c9d3 | ||
|
|
cf375cbcc4 | ||
|
|
6d6d35d598 | ||
|
|
05b257adf9 | ||
|
|
cabfef669a | ||
|
|
e4a5ed1cc4 | ||
|
|
5021997df0 | ||
|
|
d90cf0db65 | ||
|
|
dad228cd4a | ||
|
|
a652108472 | ||
|
|
5348f693fe | ||
|
|
b8c2e14e8b | ||
|
|
430b71a092 | ||
|
|
a40744e6db | ||
|
|
d802f9652a | ||
|
|
cbdb4d7ce3 | ||
|
|
691ce6a248 | ||
|
|
f9fc6944d3 | ||
|
|
e984583e5e | ||
|
|
7e03a87608 | ||
|
|
456bc481a3 | ||
|
|
b4752cd32d |
@@ -194,7 +194,9 @@ class MultiWorld():
|
|||||||
self.player_types[new_id] = NetUtils.SlotType.group
|
self.player_types[new_id] = NetUtils.SlotType.group
|
||||||
world_type = AutoWorld.AutoWorldRegister.world_types[game]
|
world_type = AutoWorld.AutoWorldRegister.world_types[game]
|
||||||
self.worlds[new_id] = world_type.create_group(self, new_id, players)
|
self.worlds[new_id] = world_type.create_group(self, new_id, players)
|
||||||
self.worlds[new_id].collect_item = classmethod(AutoWorld.World.collect_item).__get__(self.worlds[new_id])
|
self.worlds[new_id].collect_item = AutoWorld.World.collect_item.__get__(self.worlds[new_id])
|
||||||
|
self.worlds[new_id].collect = AutoWorld.World.collect.__get__(self.worlds[new_id])
|
||||||
|
self.worlds[new_id].remove = AutoWorld.World.remove.__get__(self.worlds[new_id])
|
||||||
self.player_name[new_id] = name
|
self.player_name[new_id] = name
|
||||||
|
|
||||||
new_group = self.groups[new_id] = Group(name=name, game=game, players=players,
|
new_group = self.groups[new_id] = Group(name=name, game=game, players=players,
|
||||||
@@ -342,6 +344,8 @@ class MultiWorld():
|
|||||||
region = Region("Menu", group_id, self, "ItemLink")
|
region = Region("Menu", group_id, self, "ItemLink")
|
||||||
self.regions.append(region)
|
self.regions.append(region)
|
||||||
locations = region.locations
|
locations = region.locations
|
||||||
|
# ensure that progression items are linked first, then non-progression
|
||||||
|
self.itempool.sort(key=lambda item: item.advancement)
|
||||||
for item in self.itempool:
|
for item in self.itempool:
|
||||||
count = common_item_count.get(item.player, {}).get(item.name, 0)
|
count = common_item_count.get(item.player, {}).get(item.name, 0)
|
||||||
if count:
|
if count:
|
||||||
@@ -718,7 +722,7 @@ class CollectionState():
|
|||||||
if new_region in reachable_regions:
|
if new_region in reachable_regions:
|
||||||
blocked_connections.remove(connection)
|
blocked_connections.remove(connection)
|
||||||
elif connection.can_reach(self):
|
elif connection.can_reach(self):
|
||||||
assert new_region, f"tried to search through an Entrance \"{connection}\" with no Region"
|
assert new_region, f"tried to search through an Entrance \"{connection}\" with no connected Region"
|
||||||
reachable_regions.add(new_region)
|
reachable_regions.add(new_region)
|
||||||
blocked_connections.remove(connection)
|
blocked_connections.remove(connection)
|
||||||
blocked_connections.update(new_region.exits)
|
blocked_connections.update(new_region.exits)
|
||||||
@@ -944,6 +948,7 @@ class Entrance:
|
|||||||
self.player = player
|
self.player = player
|
||||||
|
|
||||||
def can_reach(self, state: CollectionState) -> bool:
|
def can_reach(self, state: CollectionState) -> bool:
|
||||||
|
assert self.parent_region, f"called can_reach on an Entrance \"{self}\" with no parent_region"
|
||||||
if self.parent_region.can_reach(state) and self.access_rule(state):
|
if self.parent_region.can_reach(state) and self.access_rule(state):
|
||||||
if not self.hide_path and not self in state.path:
|
if not self.hide_path and not self in state.path:
|
||||||
state.path[self] = (self.name, state.path.get(self.parent_region, (self.parent_region.name, None)))
|
state.path[self] = (self.name, state.path.get(self.parent_region, (self.parent_region.name, None)))
|
||||||
@@ -1164,7 +1169,7 @@ class Location:
|
|||||||
|
|
||||||
def can_reach(self, state: CollectionState) -> bool:
|
def can_reach(self, state: CollectionState) -> bool:
|
||||||
# Region.can_reach is just a cache lookup, so placing it first for faster abort on average
|
# Region.can_reach is just a cache lookup, so placing it first for faster abort on average
|
||||||
assert self.parent_region, "Can't reach location without region"
|
assert self.parent_region, f"called can_reach on a Location \"{self}\" with no parent_region"
|
||||||
return self.parent_region.can_reach(state) and self.access_rule(state)
|
return self.parent_region.can_reach(state) and self.access_rule(state)
|
||||||
|
|
||||||
def place_locked_item(self, item: Item):
|
def place_locked_item(self, item: Item):
|
||||||
@@ -1207,7 +1212,7 @@ class ItemClassification(IntFlag):
|
|||||||
filler = 0b0000 # aka trash, as in filler items like ammo, currency etc,
|
filler = 0b0000 # aka trash, as in filler items like ammo, currency etc,
|
||||||
progression = 0b0001 # Item that is logically relevant
|
progression = 0b0001 # Item that is logically relevant
|
||||||
useful = 0b0010 # Item that is generally quite useful, but not required for anything logical
|
useful = 0b0010 # Item that is generally quite useful, but not required for anything logical
|
||||||
trap = 0b0100 # detrimental or entirely useless (nothing) item
|
trap = 0b0100 # detrimental item
|
||||||
skip_balancing = 0b1000 # should technically never occur on its own
|
skip_balancing = 0b1000 # should technically never occur on its own
|
||||||
# Item that is logically relevant, but progression balancing should not touch.
|
# Item that is logically relevant, but progression balancing should not touch.
|
||||||
# Typically currency or other counted items.
|
# Typically currency or other counted items.
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sys
|
||||||
import ModuleUpdate
|
import ModuleUpdate
|
||||||
ModuleUpdate.update()
|
ModuleUpdate.update()
|
||||||
|
|
||||||
from worlds._bizhawk.context import launch
|
from worlds._bizhawk.context import launch
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
launch()
|
launch(*sys.argv[1:])
|
||||||
|
|||||||
@@ -45,10 +45,21 @@ def get_ssl_context():
|
|||||||
|
|
||||||
|
|
||||||
class ClientCommandProcessor(CommandProcessor):
|
class ClientCommandProcessor(CommandProcessor):
|
||||||
|
"""
|
||||||
|
The Command Processor will parse every method of the class that starts with "_cmd_" as a command to be called
|
||||||
|
when parsing user input, i.e. _cmd_exit will be called when the user sends the command "/exit".
|
||||||
|
|
||||||
|
The decorator @mark_raw can be imported from MultiServer and tells the parser to only split on the first
|
||||||
|
space after the command i.e. "/exit one two three" will be passed in as method("one two three") with mark_raw
|
||||||
|
and method("one", "two", "three") without.
|
||||||
|
|
||||||
|
In addition all docstrings for command methods will be displayed to the user on launch and when using "/help"
|
||||||
|
"""
|
||||||
def __init__(self, ctx: CommonContext):
|
def __init__(self, ctx: CommonContext):
|
||||||
self.ctx = ctx
|
self.ctx = ctx
|
||||||
|
|
||||||
def output(self, text: str):
|
def output(self, text: str):
|
||||||
|
"""Helper function to abstract logging to the CommonClient UI"""
|
||||||
logger.info(text)
|
logger.info(text)
|
||||||
|
|
||||||
def _cmd_exit(self) -> bool:
|
def _cmd_exit(self) -> bool:
|
||||||
@@ -164,13 +175,14 @@ class ClientCommandProcessor(CommandProcessor):
|
|||||||
async_start(self.ctx.send_msgs([{"cmd": "StatusUpdate", "status": state}]), name="send StatusUpdate")
|
async_start(self.ctx.send_msgs([{"cmd": "StatusUpdate", "status": state}]), name="send StatusUpdate")
|
||||||
|
|
||||||
def default(self, raw: str):
|
def default(self, raw: str):
|
||||||
|
"""The default message parser to be used when parsing any messages that do not match a command"""
|
||||||
raw = self.ctx.on_user_say(raw)
|
raw = self.ctx.on_user_say(raw)
|
||||||
if raw:
|
if raw:
|
||||||
async_start(self.ctx.send_msgs([{"cmd": "Say", "text": raw}]), name="send Say")
|
async_start(self.ctx.send_msgs([{"cmd": "Say", "text": raw}]), name="send Say")
|
||||||
|
|
||||||
|
|
||||||
class CommonContext:
|
class CommonContext:
|
||||||
# Should be adjusted as needed in subclasses
|
# The following attributes are used to Connect and should be adjusted as needed in subclasses
|
||||||
tags: typing.Set[str] = {"AP"}
|
tags: typing.Set[str] = {"AP"}
|
||||||
game: typing.Optional[str] = None
|
game: typing.Optional[str] = None
|
||||||
items_handling: typing.Optional[int] = None
|
items_handling: typing.Optional[int] = None
|
||||||
@@ -429,7 +441,10 @@ class CommonContext:
|
|||||||
self.auth = await self.console_input()
|
self.auth = await self.console_input()
|
||||||
|
|
||||||
async def send_connect(self, **kwargs: typing.Any) -> None:
|
async def send_connect(self, **kwargs: typing.Any) -> None:
|
||||||
""" send `Connect` packet to log in to server """
|
"""
|
||||||
|
Send a `Connect` packet to log in to the server,
|
||||||
|
additional keyword args can override any value in the connection packet
|
||||||
|
"""
|
||||||
payload = {
|
payload = {
|
||||||
'cmd': 'Connect',
|
'cmd': 'Connect',
|
||||||
'password': self.password, 'name': self.auth, 'version': Utils.version_tuple,
|
'password': self.password, 'name': self.auth, 'version': Utils.version_tuple,
|
||||||
@@ -439,6 +454,7 @@ class CommonContext:
|
|||||||
if kwargs:
|
if kwargs:
|
||||||
payload.update(kwargs)
|
payload.update(kwargs)
|
||||||
await self.send_msgs([payload])
|
await self.send_msgs([payload])
|
||||||
|
await self.send_msgs([{"cmd": "Get", "keys": ["_read_race_mode"]}])
|
||||||
|
|
||||||
async def console_input(self) -> str:
|
async def console_input(self) -> str:
|
||||||
if self.ui:
|
if self.ui:
|
||||||
@@ -459,6 +475,7 @@ class CommonContext:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
def slot_concerns_self(self, slot) -> bool:
|
def slot_concerns_self(self, slot) -> bool:
|
||||||
|
"""Helper function to abstract player groups, should be used instead of checking slot == self.slot directly."""
|
||||||
if slot == self.slot:
|
if slot == self.slot:
|
||||||
return True
|
return True
|
||||||
if slot in self.slot_info:
|
if slot in self.slot_info:
|
||||||
@@ -466,6 +483,7 @@ class CommonContext:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
def is_echoed_chat(self, print_json_packet: dict) -> bool:
|
def is_echoed_chat(self, print_json_packet: dict) -> bool:
|
||||||
|
"""Helper function for filtering out messages sent by self."""
|
||||||
return print_json_packet.get("type", "") == "Chat" \
|
return print_json_packet.get("type", "") == "Chat" \
|
||||||
and print_json_packet.get("team", None) == self.team \
|
and print_json_packet.get("team", None) == self.team \
|
||||||
and print_json_packet.get("slot", None) == self.slot
|
and print_json_packet.get("slot", None) == self.slot
|
||||||
@@ -497,13 +515,14 @@ class CommonContext:
|
|||||||
"""Gets called before sending a Say to the server from the user.
|
"""Gets called before sending a Say to the server from the user.
|
||||||
Returned text is sent, or sending is aborted if None is returned."""
|
Returned text is sent, or sending is aborted if None is returned."""
|
||||||
return text
|
return text
|
||||||
|
|
||||||
def on_ui_command(self, text: str) -> None:
|
def on_ui_command(self, text: str) -> None:
|
||||||
"""Gets called by kivy when the user executes a command starting with `/` or `!`.
|
"""Gets called by kivy when the user executes a command starting with `/` or `!`.
|
||||||
The command processor is still called; this is just intended for command echoing."""
|
The command processor is still called; this is just intended for command echoing."""
|
||||||
self.ui.print_json([{"text": text, "type": "color", "color": "orange"}])
|
self.ui.print_json([{"text": text, "type": "color", "color": "orange"}])
|
||||||
|
|
||||||
def update_permissions(self, permissions: typing.Dict[str, int]):
|
def update_permissions(self, permissions: typing.Dict[str, int]):
|
||||||
|
"""Internal method to parse and save server permissions from RoomInfo"""
|
||||||
for permission_name, permission_flag in permissions.items():
|
for permission_name, permission_flag in permissions.items():
|
||||||
try:
|
try:
|
||||||
flag = Permission(permission_flag)
|
flag = Permission(permission_flag)
|
||||||
@@ -613,6 +632,7 @@ class CommonContext:
|
|||||||
logger.info(f"DeathLink: Received from {data['source']}")
|
logger.info(f"DeathLink: Received from {data['source']}")
|
||||||
|
|
||||||
async def send_death(self, death_text: str = ""):
|
async def send_death(self, death_text: str = ""):
|
||||||
|
"""Helper function to send a deathlink using death_text as the unique death cause string."""
|
||||||
if self.server and self.server.socket:
|
if self.server and self.server.socket:
|
||||||
logger.info("DeathLink: Sending death to your friends...")
|
logger.info("DeathLink: Sending death to your friends...")
|
||||||
self.last_death_link = time.time()
|
self.last_death_link = time.time()
|
||||||
@@ -626,6 +646,7 @@ class CommonContext:
|
|||||||
}])
|
}])
|
||||||
|
|
||||||
async def update_death_link(self, death_link: bool):
|
async def update_death_link(self, death_link: bool):
|
||||||
|
"""Helper function to set Death Link connection tag on/off and update the connection if already connected."""
|
||||||
old_tags = self.tags.copy()
|
old_tags = self.tags.copy()
|
||||||
if death_link:
|
if death_link:
|
||||||
self.tags.add("DeathLink")
|
self.tags.add("DeathLink")
|
||||||
@@ -635,7 +656,7 @@ class CommonContext:
|
|||||||
await self.send_msgs([{"cmd": "ConnectUpdate", "tags": self.tags}])
|
await self.send_msgs([{"cmd": "ConnectUpdate", "tags": self.tags}])
|
||||||
|
|
||||||
def gui_error(self, title: str, text: typing.Union[Exception, str]) -> typing.Optional["kvui.MessageBox"]:
|
def gui_error(self, title: str, text: typing.Union[Exception, str]) -> typing.Optional["kvui.MessageBox"]:
|
||||||
"""Displays an error messagebox"""
|
"""Displays an error messagebox in the loaded Kivy UI. Override if using a different UI framework"""
|
||||||
if not self.ui:
|
if not self.ui:
|
||||||
return None
|
return None
|
||||||
title = title or "Error"
|
title = title or "Error"
|
||||||
@@ -662,17 +683,19 @@ class CommonContext:
|
|||||||
logger.exception(msg, exc_info=exc_info, extra={'compact_gui': True})
|
logger.exception(msg, exc_info=exc_info, extra={'compact_gui': True})
|
||||||
self._messagebox_connection_loss = self.gui_error(msg, exc_info[1])
|
self._messagebox_connection_loss = self.gui_error(msg, exc_info[1])
|
||||||
|
|
||||||
def run_gui(self):
|
def make_gui(self) -> typing.Type["kvui.GameManager"]:
|
||||||
"""Import kivy UI system and start running it as self.ui_task."""
|
"""To return the Kivy App class needed for run_gui so it can be overridden before being built"""
|
||||||
from kvui import GameManager
|
from kvui import GameManager
|
||||||
|
|
||||||
class TextManager(GameManager):
|
class TextManager(GameManager):
|
||||||
logging_pairs = [
|
|
||||||
("Client", "Archipelago")
|
|
||||||
]
|
|
||||||
base_title = "Archipelago Text Client"
|
base_title = "Archipelago Text Client"
|
||||||
|
|
||||||
self.ui = TextManager(self)
|
return TextManager
|
||||||
|
|
||||||
|
def run_gui(self):
|
||||||
|
"""Import kivy UI system from make_gui() and start running it as self.ui_task."""
|
||||||
|
ui_class = self.make_gui()
|
||||||
|
self.ui = ui_class(self)
|
||||||
self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI")
|
self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI")
|
||||||
|
|
||||||
def run_cli(self):
|
def run_cli(self):
|
||||||
@@ -985,6 +1008,7 @@ async def console_loop(ctx: CommonContext):
|
|||||||
|
|
||||||
|
|
||||||
def get_base_parser(description: typing.Optional[str] = None):
|
def get_base_parser(description: typing.Optional[str] = None):
|
||||||
|
"""Base argument parser to be reused for components subclassing off of CommonClient"""
|
||||||
import argparse
|
import argparse
|
||||||
parser = argparse.ArgumentParser(description=description)
|
parser = argparse.ArgumentParser(description=description)
|
||||||
parser.add_argument('--connect', default=None, help='Address of the multiworld host.')
|
parser.add_argument('--connect', default=None, help='Address of the multiworld host.')
|
||||||
@@ -994,7 +1018,7 @@ def get_base_parser(description: typing.Optional[str] = None):
|
|||||||
return parser
|
return parser
|
||||||
|
|
||||||
|
|
||||||
def run_as_textclient():
|
def run_as_textclient(*args):
|
||||||
class TextContext(CommonContext):
|
class TextContext(CommonContext):
|
||||||
# Text Mode to use !hint and such with games that have no text entry
|
# Text Mode to use !hint and such with games that have no text entry
|
||||||
tags = CommonContext.tags | {"TextOnly"}
|
tags = CommonContext.tags | {"TextOnly"}
|
||||||
@@ -1033,16 +1057,21 @@ def run_as_textclient():
|
|||||||
parser = get_base_parser(description="Gameless Archipelago Client, for text interfacing.")
|
parser = get_base_parser(description="Gameless Archipelago Client, for text interfacing.")
|
||||||
parser.add_argument('--name', default=None, help="Slot Name to connect as.")
|
parser.add_argument('--name', default=None, help="Slot Name to connect as.")
|
||||||
parser.add_argument("url", nargs="?", help="Archipelago connection url")
|
parser.add_argument("url", nargs="?", help="Archipelago connection url")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args(args)
|
||||||
|
|
||||||
|
# handle if text client is launched using the "archipelago://name:pass@host:port" url from webhost
|
||||||
if args.url:
|
if args.url:
|
||||||
url = urllib.parse.urlparse(args.url)
|
url = urllib.parse.urlparse(args.url)
|
||||||
args.connect = url.netloc
|
if url.scheme == "archipelago":
|
||||||
if url.username:
|
args.connect = url.netloc
|
||||||
args.name = urllib.parse.unquote(url.username)
|
if url.username:
|
||||||
if url.password:
|
args.name = urllib.parse.unquote(url.username)
|
||||||
args.password = urllib.parse.unquote(url.password)
|
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")
|
||||||
|
|
||||||
|
# use colorama to display colored text highlighting on windows
|
||||||
colorama.init()
|
colorama.init()
|
||||||
|
|
||||||
asyncio.run(main(args))
|
asyncio.run(main(args))
|
||||||
@@ -1051,4 +1080,4 @@ def run_as_textclient():
|
|||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
logging.getLogger().setLevel(logging.INFO) # force log-level to work around log level resetting to WARNING
|
logging.getLogger().setLevel(logging.INFO) # force log-level to work around log level resetting to WARNING
|
||||||
run_as_textclient()
|
run_as_textclient(*sys.argv[1:]) # default value for parse_args
|
||||||
|
|||||||
20
Fill.py
20
Fill.py
@@ -475,28 +475,26 @@ def distribute_items_restrictive(multiworld: MultiWorld,
|
|||||||
nonlocal lock_later
|
nonlocal lock_later
|
||||||
lock_later.append(location)
|
lock_later.append(location)
|
||||||
|
|
||||||
|
single_player = multiworld.players == 1 and not multiworld.groups
|
||||||
|
|
||||||
if prioritylocations:
|
if prioritylocations:
|
||||||
# "priority fill"
|
# "priority fill"
|
||||||
fill_restrictive(multiworld, multiworld.state, prioritylocations, progitempool,
|
fill_restrictive(multiworld, multiworld.state, prioritylocations, progitempool,
|
||||||
single_player_placement=multiworld.players == 1, swap=False, on_place=mark_for_locking,
|
single_player_placement=single_player, swap=False, on_place=mark_for_locking, name="Priority")
|
||||||
name="Priority")
|
|
||||||
accessibility_corrections(multiworld, multiworld.state, prioritylocations, progitempool)
|
accessibility_corrections(multiworld, multiworld.state, prioritylocations, progitempool)
|
||||||
defaultlocations = prioritylocations + defaultlocations
|
defaultlocations = prioritylocations + defaultlocations
|
||||||
|
|
||||||
if progitempool:
|
if progitempool:
|
||||||
# "advancement/progression fill"
|
# "advancement/progression fill"
|
||||||
if panic_method == "swap":
|
if panic_method == "swap":
|
||||||
fill_restrictive(multiworld, multiworld.state, defaultlocations, progitempool,
|
fill_restrictive(multiworld, multiworld.state, defaultlocations, progitempool, swap=True,
|
||||||
swap=True,
|
name="Progression", single_player_placement=single_player)
|
||||||
name="Progression", single_player_placement=multiworld.players == 1)
|
|
||||||
elif panic_method == "raise":
|
elif panic_method == "raise":
|
||||||
fill_restrictive(multiworld, multiworld.state, defaultlocations, progitempool,
|
fill_restrictive(multiworld, multiworld.state, defaultlocations, progitempool, swap=False,
|
||||||
swap=False,
|
name="Progression", single_player_placement=single_player)
|
||||||
name="Progression", single_player_placement=multiworld.players == 1)
|
|
||||||
elif panic_method == "start_inventory":
|
elif panic_method == "start_inventory":
|
||||||
fill_restrictive(multiworld, multiworld.state, defaultlocations, progitempool,
|
fill_restrictive(multiworld, multiworld.state, defaultlocations, progitempool, swap=False,
|
||||||
swap=False, allow_partial=True,
|
allow_partial=True, name="Progression", single_player_placement=single_player)
|
||||||
name="Progression", single_player_placement=multiworld.players == 1)
|
|
||||||
if progitempool:
|
if progitempool:
|
||||||
for item in progitempool:
|
for item in progitempool:
|
||||||
logging.debug(f"Moved {item} to start_inventory to prevent fill failure.")
|
logging.debug(f"Moved {item} to start_inventory to prevent fill failure.")
|
||||||
|
|||||||
34
Generate.py
34
Generate.py
@@ -43,10 +43,10 @@ def mystery_argparse():
|
|||||||
parser.add_argument('--race', action='store_true', default=defaults.race)
|
parser.add_argument('--race', action='store_true', default=defaults.race)
|
||||||
parser.add_argument('--meta_file_path', default=defaults.meta_file_path)
|
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='info', help='Sets log level')
|
||||||
parser.add_argument('--yaml_output', default=0, type=lambda value: max(int(value), 0),
|
parser.add_argument("--csv_output", action="store_true",
|
||||||
help='Output rolled mystery results to yaml up to specified number (made for async multiworld)')
|
help="Output rolled player options to csv (made for async multiworld).")
|
||||||
parser.add_argument('--plando', default=defaults.plando_options,
|
parser.add_argument("--plando", default=defaults.plando_options,
|
||||||
help='List of options that can be set manually. Can be combined, for example "bosses, items"')
|
help="List of options that can be set manually. Can be combined, for example \"bosses, items\"")
|
||||||
parser.add_argument("--skip_prog_balancing", action="store_true",
|
parser.add_argument("--skip_prog_balancing", action="store_true",
|
||||||
help="Skip progression balancing step during generation.")
|
help="Skip progression balancing step during generation.")
|
||||||
parser.add_argument("--skip_output", action="store_true",
|
parser.add_argument("--skip_output", action="store_true",
|
||||||
@@ -155,6 +155,8 @@ def main(args=None) -> Tuple[argparse.Namespace, int]:
|
|||||||
erargs.outputpath = args.outputpath
|
erargs.outputpath = args.outputpath
|
||||||
erargs.skip_prog_balancing = args.skip_prog_balancing
|
erargs.skip_prog_balancing = args.skip_prog_balancing
|
||||||
erargs.skip_output = args.skip_output
|
erargs.skip_output = args.skip_output
|
||||||
|
erargs.name = {}
|
||||||
|
erargs.csv_output = args.csv_output
|
||||||
|
|
||||||
settings_cache: Dict[str, Tuple[argparse.Namespace, ...]] = \
|
settings_cache: Dict[str, Tuple[argparse.Namespace, ...]] = \
|
||||||
{fname: (tuple(roll_settings(yaml, args.plando) for yaml in yamls) if args.sameoptions else None)
|
{fname: (tuple(roll_settings(yaml, args.plando) for yaml in yamls) if args.sameoptions else None)
|
||||||
@@ -202,7 +204,7 @@ def main(args=None) -> Tuple[argparse.Namespace, int]:
|
|||||||
|
|
||||||
if path == args.weights_file_path: # if name came from the weights file, just use base player name
|
if path == args.weights_file_path: # if name came from the weights file, just use base player name
|
||||||
erargs.name[player] = f"Player{player}"
|
erargs.name[player] = f"Player{player}"
|
||||||
elif not erargs.name[player]: # if name was not specified, generate it from filename
|
elif player not in erargs.name: # if name was not specified, generate it from filename
|
||||||
erargs.name[player] = os.path.splitext(os.path.split(path)[-1])[0]
|
erargs.name[player] = os.path.splitext(os.path.split(path)[-1])[0]
|
||||||
erargs.name[player] = handle_name(erargs.name[player], player, name_counter)
|
erargs.name[player] = handle_name(erargs.name[player], player, name_counter)
|
||||||
|
|
||||||
@@ -215,28 +217,6 @@ def main(args=None) -> Tuple[argparse.Namespace, int]:
|
|||||||
if len(set(name.lower() for name in erargs.name.values())) != len(erargs.name):
|
if len(set(name.lower() for name in erargs.name.values())) != len(erargs.name):
|
||||||
raise Exception(f"Names have to be unique. Names: {Counter(name.lower() for name in erargs.name.values())}")
|
raise Exception(f"Names have to be unique. Names: {Counter(name.lower() for name in erargs.name.values())}")
|
||||||
|
|
||||||
if args.yaml_output:
|
|
||||||
import yaml
|
|
||||||
important = {}
|
|
||||||
for option, player_settings in vars(erargs).items():
|
|
||||||
if type(player_settings) == dict:
|
|
||||||
if all(type(value) != list for value in player_settings.values()):
|
|
||||||
if len(player_settings.values()) > 1:
|
|
||||||
important[option] = {player: value for player, value in player_settings.items() if
|
|
||||||
player <= args.yaml_output}
|
|
||||||
else:
|
|
||||||
logging.debug(f"No player settings defined for option '{option}'")
|
|
||||||
|
|
||||||
else:
|
|
||||||
if player_settings != "": # is not empty name
|
|
||||||
important[option] = player_settings
|
|
||||||
else:
|
|
||||||
logging.debug(f"No player settings defined for option '{option}'")
|
|
||||||
if args.outputpath:
|
|
||||||
os.makedirs(args.outputpath, exist_ok=True)
|
|
||||||
with open(os.path.join(args.outputpath if args.outputpath else ".", f"generate_{seed_name}.yaml"), "wt") as f:
|
|
||||||
yaml.dump(important, f)
|
|
||||||
|
|
||||||
return erargs, seed
|
return erargs, seed
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
103
Launcher.py
103
Launcher.py
@@ -16,10 +16,11 @@ import multiprocessing
|
|||||||
import shlex
|
import shlex
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
import urllib.parse
|
||||||
import webbrowser
|
import webbrowser
|
||||||
from os.path import isfile
|
from os.path import isfile
|
||||||
from shutil import which
|
from shutil import which
|
||||||
from typing import Callable, Sequence, Union, Optional
|
from typing import Callable, Optional, Sequence, Tuple, Union
|
||||||
|
|
||||||
import Utils
|
import Utils
|
||||||
import settings
|
import settings
|
||||||
@@ -107,7 +108,81 @@ components.extend([
|
|||||||
])
|
])
|
||||||
|
|
||||||
|
|
||||||
def identify(path: Union[None, str]):
|
def handle_uri(path: str, launch_args: Tuple[str, ...]) -> None:
|
||||||
|
url = urllib.parse.urlparse(path)
|
||||||
|
queries = urllib.parse.parse_qs(url.query)
|
||||||
|
launch_args = (path, *launch_args)
|
||||||
|
client_component = None
|
||||||
|
text_client_component = None
|
||||||
|
if "game" in queries:
|
||||||
|
game = queries["game"][0]
|
||||||
|
else: # TODO around 0.6.0 - this is for pre this change webhost uri's
|
||||||
|
game = "Archipelago"
|
||||||
|
for component in components:
|
||||||
|
if component.supports_uri and component.game_name == game:
|
||||||
|
client_component = component
|
||||||
|
elif component.display_name == "Text Client":
|
||||||
|
text_client_component = component
|
||||||
|
|
||||||
|
from kvui import App, Button, BoxLayout, Label, Clock, Window
|
||||||
|
|
||||||
|
class Popup(App):
|
||||||
|
timer_label: Label
|
||||||
|
remaining_time: Optional[int]
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.title = "Connect to Multiworld"
|
||||||
|
self.icon = r"data/icon.png"
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
def build(self):
|
||||||
|
layout = BoxLayout(orientation="vertical")
|
||||||
|
|
||||||
|
if client_component is None:
|
||||||
|
self.remaining_time = 7
|
||||||
|
label_text = (f"A game client able to parse URIs was not detected for {game}.\n"
|
||||||
|
f"Launching Text Client in 7 seconds...")
|
||||||
|
self.timer_label = Label(text=label_text)
|
||||||
|
layout.add_widget(self.timer_label)
|
||||||
|
Clock.schedule_interval(self.update_label, 1)
|
||||||
|
else:
|
||||||
|
layout.add_widget(Label(text="Select client to open and connect with."))
|
||||||
|
button_row = BoxLayout(orientation="horizontal", size_hint=(1, 0.4))
|
||||||
|
|
||||||
|
text_client_button = Button(
|
||||||
|
text=text_client_component.display_name,
|
||||||
|
on_release=lambda *args: run_component(text_client_component, *launch_args)
|
||||||
|
)
|
||||||
|
button_row.add_widget(text_client_button)
|
||||||
|
|
||||||
|
game_client_button = Button(
|
||||||
|
text=client_component.display_name,
|
||||||
|
on_release=lambda *args: run_component(client_component, *launch_args)
|
||||||
|
)
|
||||||
|
button_row.add_widget(game_client_button)
|
||||||
|
|
||||||
|
layout.add_widget(button_row)
|
||||||
|
|
||||||
|
return layout
|
||||||
|
|
||||||
|
def update_label(self, dt):
|
||||||
|
if self.remaining_time > 1:
|
||||||
|
# countdown the timer and string replace the number
|
||||||
|
self.remaining_time -= 1
|
||||||
|
self.timer_label.text = self.timer_label.text.replace(
|
||||||
|
str(self.remaining_time + 1), str(self.remaining_time)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# our timer is finished so launch text client and close down
|
||||||
|
run_component(text_client_component, *launch_args)
|
||||||
|
Clock.unschedule(self.update_label)
|
||||||
|
App.get_running_app().stop()
|
||||||
|
Window.close()
|
||||||
|
|
||||||
|
Popup().run()
|
||||||
|
|
||||||
|
|
||||||
|
def identify(path: Union[None, str]) -> Tuple[Union[None, str], Union[None, Component]]:
|
||||||
if path is None:
|
if path is None:
|
||||||
return None, None
|
return None, None
|
||||||
for component in components:
|
for component in components:
|
||||||
@@ -299,20 +374,24 @@ def main(args: Optional[Union[argparse.Namespace, dict]] = None):
|
|||||||
elif not args:
|
elif not args:
|
||||||
args = {}
|
args = {}
|
||||||
|
|
||||||
if args.get("Patch|Game|Component", None) is not None:
|
path = args.get("Patch|Game|Component|url", None)
|
||||||
file, component = identify(args["Patch|Game|Component"])
|
if path is not None:
|
||||||
|
if path.startswith("archipelago://"):
|
||||||
|
handle_uri(path, args.get("args", ()))
|
||||||
|
return
|
||||||
|
file, component = identify(path)
|
||||||
if file:
|
if file:
|
||||||
args['file'] = file
|
args['file'] = file
|
||||||
if component:
|
if component:
|
||||||
args['component'] = component
|
args['component'] = component
|
||||||
if not component:
|
if not component:
|
||||||
logging.warning(f"Could not identify Component responsible for {args['Patch|Game|Component']}")
|
logging.warning(f"Could not identify Component responsible for {path}")
|
||||||
|
|
||||||
if args["update_settings"]:
|
if args["update_settings"]:
|
||||||
update_settings()
|
update_settings()
|
||||||
if 'file' in args:
|
if "file" in args:
|
||||||
run_component(args["component"], args["file"], *args["args"])
|
run_component(args["component"], args["file"], *args["args"])
|
||||||
elif 'component' in args:
|
elif "component" in args:
|
||||||
run_component(args["component"], *args["args"])
|
run_component(args["component"], *args["args"])
|
||||||
elif not args["update_settings"]:
|
elif not args["update_settings"]:
|
||||||
run_gui()
|
run_gui()
|
||||||
@@ -322,12 +401,16 @@ if __name__ == '__main__':
|
|||||||
init_logging('Launcher')
|
init_logging('Launcher')
|
||||||
Utils.freeze_support()
|
Utils.freeze_support()
|
||||||
multiprocessing.set_start_method("spawn") # if launched process uses kivy, fork won't work
|
multiprocessing.set_start_method("spawn") # if launched process uses kivy, fork won't work
|
||||||
parser = argparse.ArgumentParser(description='Archipelago Launcher')
|
parser = argparse.ArgumentParser(
|
||||||
|
description='Archipelago Launcher',
|
||||||
|
usage="[-h] [--update_settings] [Patch|Game|Component] [-- component args here]"
|
||||||
|
)
|
||||||
run_group = parser.add_argument_group("Run")
|
run_group = parser.add_argument_group("Run")
|
||||||
run_group.add_argument("--update_settings", action="store_true",
|
run_group.add_argument("--update_settings", action="store_true",
|
||||||
help="Update host.yaml and exit.")
|
help="Update host.yaml and exit.")
|
||||||
run_group.add_argument("Patch|Game|Component", type=str, nargs="?",
|
run_group.add_argument("Patch|Game|Component|url", type=str, nargs="?",
|
||||||
help="Pass either a patch file, a generated game or the name of a component to run.")
|
help="Pass either a patch file, a generated game, the component name to run, or a url to "
|
||||||
|
"connect with.")
|
||||||
run_group.add_argument("args", nargs="*",
|
run_group.add_argument("args", nargs="*",
|
||||||
help="Arguments to pass to component.")
|
help="Arguments to pass to component.")
|
||||||
main(parser.parse_args())
|
main(parser.parse_args())
|
||||||
|
|||||||
@@ -467,6 +467,8 @@ class LinksAwakeningContext(CommonContext):
|
|||||||
|
|
||||||
def __init__(self, server_address: typing.Optional[str], password: typing.Optional[str], magpie: typing.Optional[bool]) -> None:
|
def __init__(self, server_address: typing.Optional[str], password: typing.Optional[str], magpie: typing.Optional[bool]) -> None:
|
||||||
self.client = LinksAwakeningClient()
|
self.client = LinksAwakeningClient()
|
||||||
|
self.slot_data = {}
|
||||||
|
|
||||||
if magpie:
|
if magpie:
|
||||||
self.magpie_enabled = True
|
self.magpie_enabled = True
|
||||||
self.magpie = MagpieBridge()
|
self.magpie = MagpieBridge()
|
||||||
@@ -564,6 +566,8 @@ class LinksAwakeningContext(CommonContext):
|
|||||||
def on_package(self, cmd: str, args: dict):
|
def on_package(self, cmd: str, args: dict):
|
||||||
if cmd == "Connected":
|
if cmd == "Connected":
|
||||||
self.game = self.slot_info[self.slot].game
|
self.game = self.slot_info[self.slot].game
|
||||||
|
self.slot_data = args.get("slot_data", {})
|
||||||
|
|
||||||
# TODO - use watcher_event
|
# TODO - use watcher_event
|
||||||
if cmd == "ReceivedItems":
|
if cmd == "ReceivedItems":
|
||||||
for index, item in enumerate(args["items"], start=args["index"]):
|
for index, item in enumerate(args["items"], start=args["index"]):
|
||||||
@@ -628,6 +632,7 @@ class LinksAwakeningContext(CommonContext):
|
|||||||
self.magpie.set_checks(self.client.tracker.all_checks)
|
self.magpie.set_checks(self.client.tracker.all_checks)
|
||||||
await self.magpie.set_item_tracker(self.client.item_tracker)
|
await self.magpie.set_item_tracker(self.client.item_tracker)
|
||||||
await self.magpie.send_gps(self.client.gps_tracker)
|
await self.magpie.send_gps(self.client.gps_tracker)
|
||||||
|
self.magpie.slot_data = self.slot_data
|
||||||
except Exception:
|
except Exception:
|
||||||
# Don't let magpie errors take out the client
|
# Don't let magpie errors take out the client
|
||||||
pass
|
pass
|
||||||
|
|||||||
4
Main.py
4
Main.py
@@ -46,6 +46,9 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
|||||||
multiworld.sprite_pool = args.sprite_pool.copy()
|
multiworld.sprite_pool = args.sprite_pool.copy()
|
||||||
|
|
||||||
multiworld.set_options(args)
|
multiworld.set_options(args)
|
||||||
|
if args.csv_output:
|
||||||
|
from Options import dump_player_options
|
||||||
|
dump_player_options(multiworld)
|
||||||
multiworld.set_item_links()
|
multiworld.set_item_links()
|
||||||
multiworld.state = CollectionState(multiworld)
|
multiworld.state = CollectionState(multiworld)
|
||||||
logger.info('Archipelago Version %s - Seed: %s\n', __version__, multiworld.seed)
|
logger.info('Archipelago Version %s - Seed: %s\n', __version__, multiworld.seed)
|
||||||
@@ -335,6 +338,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
|||||||
"seed_name": multiworld.seed_name,
|
"seed_name": multiworld.seed_name,
|
||||||
"spheres": spheres,
|
"spheres": spheres,
|
||||||
"datapackage": data_package,
|
"datapackage": data_package,
|
||||||
|
"race_mode": int(multiworld.is_race),
|
||||||
}
|
}
|
||||||
AutoWorld.call_all(multiworld, "modify_multidata", multidata)
|
AutoWorld.call_all(multiworld, "modify_multidata", multidata)
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import math
|
|||||||
import operator
|
import operator
|
||||||
import pickle
|
import pickle
|
||||||
import random
|
import random
|
||||||
|
import shlex
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
import typing
|
import typing
|
||||||
@@ -427,6 +428,8 @@ class Context:
|
|||||||
use_embedded_server_options: bool):
|
use_embedded_server_options: bool):
|
||||||
|
|
||||||
self.read_data = {}
|
self.read_data = {}
|
||||||
|
# there might be a better place to put this.
|
||||||
|
self.read_data["race_mode"] = lambda: decoded_obj.get("race_mode", 0)
|
||||||
mdata_ver = decoded_obj["minimum_versions"]["server"]
|
mdata_ver = decoded_obj["minimum_versions"]["server"]
|
||||||
if mdata_ver > version_tuple:
|
if mdata_ver > version_tuple:
|
||||||
raise RuntimeError(f"Supplied Multidata (.archipelago) requires a server of at least version {mdata_ver},"
|
raise RuntimeError(f"Supplied Multidata (.archipelago) requires a server of at least version {mdata_ver},"
|
||||||
@@ -1150,7 +1153,7 @@ class CommandProcessor(metaclass=CommandMeta):
|
|||||||
if not raw:
|
if not raw:
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
command = raw.split()
|
command = shlex.split(raw, comments=False)
|
||||||
basecommand = command[0]
|
basecommand = command[0]
|
||||||
if basecommand[0] == self.marker:
|
if basecommand[0] == self.marker:
|
||||||
method = self.commands.get(basecommand[1:].lower(), None)
|
method = self.commands.get(basecommand[1:].lower(), None)
|
||||||
|
|||||||
@@ -273,7 +273,8 @@ class RawJSONtoTextParser(JSONtoTextParser):
|
|||||||
|
|
||||||
color_codes = {'reset': 0, 'bold': 1, 'underline': 4, 'black': 30, 'red': 31, 'green': 32, 'yellow': 33, 'blue': 34,
|
color_codes = {'reset': 0, 'bold': 1, 'underline': 4, 'black': 30, 'red': 31, 'green': 32, 'yellow': 33, 'blue': 34,
|
||||||
'magenta': 35, 'cyan': 36, 'white': 37, 'black_bg': 40, 'red_bg': 41, 'green_bg': 42, 'yellow_bg': 43,
|
'magenta': 35, 'cyan': 36, 'white': 37, 'black_bg': 40, 'red_bg': 41, 'green_bg': 42, 'yellow_bg': 43,
|
||||||
'blue_bg': 44, 'magenta_bg': 45, 'cyan_bg': 46, 'white_bg': 47}
|
'blue_bg': 44, 'magenta_bg': 45, 'cyan_bg': 46, 'white_bg': 47,
|
||||||
|
'plum': 35, 'slateblue': 34, 'salmon': 31,} # convert ui colors to terminal colors
|
||||||
|
|
||||||
|
|
||||||
def color_code(*args):
|
def color_code(*args):
|
||||||
|
|||||||
60
Options.py
60
Options.py
@@ -8,16 +8,17 @@ import numbers
|
|||||||
import random
|
import random
|
||||||
import typing
|
import typing
|
||||||
import enum
|
import enum
|
||||||
|
from collections import defaultdict
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
||||||
from schema import And, Optional, Or, Schema
|
from schema import And, Optional, Or, Schema
|
||||||
from typing_extensions import Self
|
from typing_extensions import Self
|
||||||
|
|
||||||
from Utils import get_fuzzy_results, is_iterable_except_str
|
from Utils import get_fuzzy_results, is_iterable_except_str, output_path
|
||||||
|
|
||||||
if typing.TYPE_CHECKING:
|
if typing.TYPE_CHECKING:
|
||||||
from BaseClasses import PlandoOptions
|
from BaseClasses import MultiWorld, PlandoOptions
|
||||||
from worlds.AutoWorld import World
|
from worlds.AutoWorld import World
|
||||||
import pathlib
|
import pathlib
|
||||||
|
|
||||||
@@ -973,7 +974,19 @@ class PlandoTexts(Option[typing.List[PlandoText]], VerifyKeys):
|
|||||||
if random.random() < float(text.get("percentage", 100)/100):
|
if random.random() < float(text.get("percentage", 100)/100):
|
||||||
at = text.get("at", None)
|
at = text.get("at", None)
|
||||||
if at is not None:
|
if at is not None:
|
||||||
|
if isinstance(at, dict):
|
||||||
|
if at:
|
||||||
|
at = random.choices(list(at.keys()),
|
||||||
|
weights=list(at.values()), k=1)[0]
|
||||||
|
else:
|
||||||
|
raise OptionError("\"at\" must be a valid string or weighted list of strings!")
|
||||||
given_text = text.get("text", [])
|
given_text = text.get("text", [])
|
||||||
|
if isinstance(given_text, dict):
|
||||||
|
if not given_text:
|
||||||
|
given_text = []
|
||||||
|
else:
|
||||||
|
given_text = random.choices(list(given_text.keys()),
|
||||||
|
weights=list(given_text.values()), k=1)
|
||||||
if isinstance(given_text, str):
|
if isinstance(given_text, str):
|
||||||
given_text = [given_text]
|
given_text = [given_text]
|
||||||
texts.append(PlandoText(
|
texts.append(PlandoText(
|
||||||
@@ -981,6 +994,8 @@ class PlandoTexts(Option[typing.List[PlandoText]], VerifyKeys):
|
|||||||
given_text,
|
given_text,
|
||||||
text.get("percentage", 100)
|
text.get("percentage", 100)
|
||||||
))
|
))
|
||||||
|
else:
|
||||||
|
raise OptionError("\"at\" must be a valid string or weighted list of strings!")
|
||||||
elif isinstance(text, PlandoText):
|
elif isinstance(text, PlandoText):
|
||||||
if random.random() < float(text.percentage/100):
|
if random.random() < float(text.percentage/100):
|
||||||
texts.append(text)
|
texts.append(text)
|
||||||
@@ -1321,7 +1336,7 @@ class PriorityLocations(LocationSet):
|
|||||||
|
|
||||||
|
|
||||||
class DeathLink(Toggle):
|
class DeathLink(Toggle):
|
||||||
"""When you die, everyone dies. Of course the reverse is true too."""
|
"""When you die, everyone who enabled death link dies. Of course, the reverse is true too."""
|
||||||
display_name = "Death Link"
|
display_name = "Death Link"
|
||||||
rich_text_doc = True
|
rich_text_doc = True
|
||||||
|
|
||||||
@@ -1518,3 +1533,42 @@ def generate_yaml_templates(target_folder: typing.Union[str, "pathlib.Path"], ge
|
|||||||
|
|
||||||
with open(os.path.join(target_folder, game_name + ".yaml"), "w", encoding="utf-8-sig") as f:
|
with open(os.path.join(target_folder, game_name + ".yaml"), "w", encoding="utf-8-sig") as f:
|
||||||
f.write(res)
|
f.write(res)
|
||||||
|
|
||||||
|
|
||||||
|
def dump_player_options(multiworld: MultiWorld) -> None:
|
||||||
|
from csv import DictWriter
|
||||||
|
|
||||||
|
game_players = defaultdict(list)
|
||||||
|
for player, game in multiworld.game.items():
|
||||||
|
game_players[game].append(player)
|
||||||
|
game_players = dict(sorted(game_players.items()))
|
||||||
|
|
||||||
|
output = []
|
||||||
|
per_game_option_names = [
|
||||||
|
getattr(option, "display_name", option_key)
|
||||||
|
for option_key, option in PerGameCommonOptions.type_hints.items()
|
||||||
|
]
|
||||||
|
all_option_names = per_game_option_names.copy()
|
||||||
|
for game, players in game_players.items():
|
||||||
|
game_option_names = per_game_option_names.copy()
|
||||||
|
for player in players:
|
||||||
|
world = multiworld.worlds[player]
|
||||||
|
player_output = {
|
||||||
|
"Game": multiworld.game[player],
|
||||||
|
"Name": multiworld.get_player_name(player),
|
||||||
|
}
|
||||||
|
output.append(player_output)
|
||||||
|
for option_key, option in world.options_dataclass.type_hints.items():
|
||||||
|
if issubclass(Removed, option):
|
||||||
|
continue
|
||||||
|
display_name = getattr(option, "display_name", option_key)
|
||||||
|
player_output[display_name] = getattr(world.options, option_key).current_option_name
|
||||||
|
if display_name not in game_option_names:
|
||||||
|
all_option_names.append(display_name)
|
||||||
|
game_option_names.append(display_name)
|
||||||
|
|
||||||
|
with open(output_path(f"generate_{multiworld.seed_name}.csv"), mode="w", newline="") as file:
|
||||||
|
fields = ["Game", "Name", *all_option_names]
|
||||||
|
writer = DictWriter(file, fields)
|
||||||
|
writer.writeheader()
|
||||||
|
writer.writerows(output)
|
||||||
|
|||||||
2
Utils.py
2
Utils.py
@@ -46,7 +46,7 @@ class Version(typing.NamedTuple):
|
|||||||
return ".".join(str(item) for item in self)
|
return ".".join(str(item) for item in self)
|
||||||
|
|
||||||
|
|
||||||
__version__ = "0.5.0"
|
__version__ = "0.5.1"
|
||||||
version_tuple = tuplize_version(__version__)
|
version_tuple = tuplize_version(__version__)
|
||||||
|
|
||||||
is_linux = sys.platform.startswith("linux")
|
is_linux = sys.platform.startswith("linux")
|
||||||
|
|||||||
@@ -267,9 +267,7 @@ class WargrooveContext(CommonContext):
|
|||||||
|
|
||||||
def build(self):
|
def build(self):
|
||||||
container = super().build()
|
container = super().build()
|
||||||
panel = TabbedPanelItem(text="Wargroove")
|
self.add_client_tab("Wargroove", self.build_tracker())
|
||||||
panel.content = self.build_tracker()
|
|
||||||
self.tabs.add_widget(panel)
|
|
||||||
return container
|
return container
|
||||||
|
|
||||||
def build_tracker(self) -> TrackerLayout:
|
def build_tracker(self) -> TrackerLayout:
|
||||||
|
|||||||
@@ -1,51 +1,15 @@
|
|||||||
"""API endpoints package."""
|
"""API endpoints package."""
|
||||||
from typing import List, Tuple
|
from typing import List, Tuple
|
||||||
from uuid import UUID
|
|
||||||
|
|
||||||
from flask import Blueprint, abort, url_for
|
from flask import Blueprint
|
||||||
|
|
||||||
import worlds.Files
|
from ..models import Seed
|
||||||
from ..models import Room, Seed
|
|
||||||
|
|
||||||
api_endpoints = Blueprint('api', __name__, url_prefix="/api")
|
api_endpoints = Blueprint('api', __name__, url_prefix="/api")
|
||||||
|
|
||||||
# unsorted/misc endpoints
|
|
||||||
|
|
||||||
|
|
||||||
def get_players(seed: Seed) -> List[Tuple[str, str]]:
|
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]
|
||||||
|
|
||||||
|
|
||||||
@api_endpoints.route('/room_status/<suuid:room>')
|
from . import datapackage, generate, room, user # trigger registration
|
||||||
def room_info(room: UUID):
|
|
||||||
room = Room.get(id=room)
|
|
||||||
if room is None:
|
|
||||||
return abort(404)
|
|
||||||
|
|
||||||
def supports_apdeltapatch(game: str):
|
|
||||||
return game in worlds.Files.AutoPatchRegister.patch_types
|
|
||||||
downloads = []
|
|
||||||
for slot in sorted(room.seed.slots):
|
|
||||||
if slot.data and not supports_apdeltapatch(slot.game):
|
|
||||||
slot_download = {
|
|
||||||
"slot": slot.player_id,
|
|
||||||
"download": url_for("download_slot_file", room_id=room.id, player_id=slot.player_id)
|
|
||||||
}
|
|
||||||
downloads.append(slot_download)
|
|
||||||
elif slot.data:
|
|
||||||
slot_download = {
|
|
||||||
"slot": slot.player_id,
|
|
||||||
"download": url_for("download_patch", patch_id=slot.id, room_id=room.id)
|
|
||||||
}
|
|
||||||
downloads.append(slot_download)
|
|
||||||
return {
|
|
||||||
"tracker": room.tracker,
|
|
||||||
"players": get_players(room.seed),
|
|
||||||
"last_port": room.last_port,
|
|
||||||
"last_activity": room.last_activity,
|
|
||||||
"timeout": room.timeout,
|
|
||||||
"downloads": downloads,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
from . import generate, user, datapackage # trigger registration
|
|
||||||
|
|||||||
42
WebHostLib/api/room.py
Normal file
42
WebHostLib/api/room.py
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
from typing import Any, Dict
|
||||||
|
from uuid import UUID
|
||||||
|
|
||||||
|
from flask import abort, url_for
|
||||||
|
|
||||||
|
import worlds.Files
|
||||||
|
from . import api_endpoints, get_players
|
||||||
|
from ..models import Room
|
||||||
|
|
||||||
|
|
||||||
|
@api_endpoints.route('/room_status/<suuid:room_id>')
|
||||||
|
def room_info(room_id: UUID) -> Dict[str, Any]:
|
||||||
|
room = Room.get(id=room_id)
|
||||||
|
if room is None:
|
||||||
|
return abort(404)
|
||||||
|
|
||||||
|
def supports_apdeltapatch(game: str) -> bool:
|
||||||
|
return game in worlds.Files.AutoPatchRegister.patch_types
|
||||||
|
|
||||||
|
downloads = []
|
||||||
|
for slot in sorted(room.seed.slots):
|
||||||
|
if slot.data and not supports_apdeltapatch(slot.game):
|
||||||
|
slot_download = {
|
||||||
|
"slot": slot.player_id,
|
||||||
|
"download": url_for("download_slot_file", room_id=room.id, player_id=slot.player_id)
|
||||||
|
}
|
||||||
|
downloads.append(slot_download)
|
||||||
|
elif slot.data:
|
||||||
|
slot_download = {
|
||||||
|
"slot": slot.player_id,
|
||||||
|
"download": url_for("download_patch", patch_id=slot.id, room_id=room.id)
|
||||||
|
}
|
||||||
|
downloads.append(slot_download)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"tracker": room.tracker,
|
||||||
|
"players": get_players(room.seed),
|
||||||
|
"last_port": room.last_port,
|
||||||
|
"last_activity": room.last_activity,
|
||||||
|
"timeout": room.timeout,
|
||||||
|
"downloads": downloads,
|
||||||
|
}
|
||||||
@@ -81,6 +81,7 @@ def start_generation(options: Dict[str, Union[dict, str]], meta: Dict[str, Any])
|
|||||||
elif len(gen_options) > app.config["MAX_ROLL"]:
|
elif len(gen_options) > app.config["MAX_ROLL"]:
|
||||||
flash(f"Sorry, generating of multiworlds is limited to {app.config['MAX_ROLL']} players. "
|
flash(f"Sorry, generating of multiworlds is limited to {app.config['MAX_ROLL']} players. "
|
||||||
f"If you have a larger group, please generate it yourself and upload it.")
|
f"If you have a larger group, please generate it yourself and upload it.")
|
||||||
|
return redirect(url_for(request.endpoint, **(request.view_args or {})))
|
||||||
elif len(gen_options) >= app.config["JOB_THRESHOLD"]:
|
elif len(gen_options) >= app.config["JOB_THRESHOLD"]:
|
||||||
gen = Generation(
|
gen = Generation(
|
||||||
options=pickle.dumps({name: vars(options) for name, options in gen_options.items()}),
|
options=pickle.dumps({name: vars(options) for name, options in gen_options.items()}),
|
||||||
@@ -134,6 +135,7 @@ def gen_game(gen_options: dict, meta: Optional[Dict[str, Any]] = None, owner=Non
|
|||||||
{"bosses", "items", "connections", "texts"}))
|
{"bosses", "items", "connections", "texts"}))
|
||||||
erargs.skip_prog_balancing = False
|
erargs.skip_prog_balancing = False
|
||||||
erargs.skip_output = False
|
erargs.skip_output = False
|
||||||
|
erargs.csv_output = False
|
||||||
|
|
||||||
name_counter = Counter()
|
name_counter = Counter()
|
||||||
for player, (playerfile, settings) in enumerate(gen_options.items(), 1):
|
for player, (playerfile, settings) in enumerate(gen_options.items(), 1):
|
||||||
|
|||||||
@@ -132,26 +132,41 @@ def display_log(room: UUID) -> Union[str, Response, Tuple[str, int]]:
|
|||||||
return "Access Denied", 403
|
return "Access Denied", 403
|
||||||
|
|
||||||
|
|
||||||
@app.route('/room/<suuid:room>', methods=['GET', 'POST'])
|
@app.post("/room/<suuid:room>")
|
||||||
|
def host_room_command(room: UUID):
|
||||||
|
room: Room = Room.get(id=room)
|
||||||
|
if room is None:
|
||||||
|
return abort(404)
|
||||||
|
|
||||||
|
if room.owner == session["_id"]:
|
||||||
|
cmd = request.form["cmd"]
|
||||||
|
if cmd:
|
||||||
|
Command(room=room, commandtext=cmd)
|
||||||
|
commit()
|
||||||
|
return redirect(url_for("host_room", room=room.id))
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/room/<suuid:room>")
|
||||||
def host_room(room: UUID):
|
def host_room(room: UUID):
|
||||||
room: Room = Room.get(id=room)
|
room: Room = Room.get(id=room)
|
||||||
if room is None:
|
if room is None:
|
||||||
return abort(404)
|
return abort(404)
|
||||||
if request.method == "POST":
|
|
||||||
if room.owner == session["_id"]:
|
|
||||||
cmd = request.form["cmd"]
|
|
||||||
if cmd:
|
|
||||||
Command(room=room, commandtext=cmd)
|
|
||||||
commit()
|
|
||||||
return redirect(url_for("host_room", room=room.id))
|
|
||||||
|
|
||||||
now = datetime.datetime.utcnow()
|
now = datetime.datetime.utcnow()
|
||||||
# indicate that the page should reload to get the assigned port
|
# indicate that the page should reload to get the assigned port
|
||||||
should_refresh = not room.last_port and now - room.creation_time < datetime.timedelta(seconds=3)
|
should_refresh = ((not room.last_port and now - room.creation_time < datetime.timedelta(seconds=3))
|
||||||
|
or room.last_activity < now - datetime.timedelta(seconds=room.timeout))
|
||||||
with db_session:
|
with db_session:
|
||||||
room.last_activity = now # will trigger a spinup, if it's not already running
|
room.last_activity = now # will trigger a spinup, if it's not already running
|
||||||
|
|
||||||
def get_log(max_size: int = 1024000) -> str:
|
browser_tokens = "Mozilla", "Chrome", "Safari"
|
||||||
|
automated = ("update" in request.args
|
||||||
|
or "Discordbot" in request.user_agent.string
|
||||||
|
or not any(browser_token in request.user_agent.string for browser_token in browser_tokens))
|
||||||
|
|
||||||
|
def get_log(max_size: int = 0 if automated else 1024000) -> str:
|
||||||
|
if max_size == 0:
|
||||||
|
return "…"
|
||||||
try:
|
try:
|
||||||
with open(os.path.join("logs", str(room.id) + ".txt"), "rb") as log:
|
with open(os.path.join("logs", str(room.id) + ".txt"), "rb") as log:
|
||||||
raw_size = 0
|
raw_size = 0
|
||||||
|
|||||||
@@ -77,4 +77,4 @@ There, you will find examples of games in the `worlds` folder:
|
|||||||
You may also find developer documentation in the `docs` folder:
|
You may also find developer documentation in the `docs` folder:
|
||||||
[/docs Folder in Archipelago Code](https://github.com/ArchipelagoMW/Archipelago/tree/main/docs).
|
[/docs Folder in Archipelago Code](https://github.com/ArchipelagoMW/Archipelago/tree/main/docs).
|
||||||
|
|
||||||
If you have more questions, feel free to ask in the **#archipelago-dev** channel on our Discord.
|
If you have more questions, feel free to ask in the **#ap-world-dev** channel on our Discord.
|
||||||
|
|||||||
@@ -58,3 +58,28 @@
|
|||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
max-height: 400px;
|
max-height: 400px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.loader{
|
||||||
|
display: inline-block;
|
||||||
|
visibility: hidden;
|
||||||
|
margin-left: 5px;
|
||||||
|
width: 40px;
|
||||||
|
aspect-ratio: 4;
|
||||||
|
--_g: no-repeat radial-gradient(circle closest-side,#fff 90%,#fff0);
|
||||||
|
background:
|
||||||
|
var(--_g) 0 50%,
|
||||||
|
var(--_g) 50% 50%,
|
||||||
|
var(--_g) 100% 50%;
|
||||||
|
background-size: calc(100%/3) 100%;
|
||||||
|
animation: l7 1s infinite linear;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loader.loading{
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes l7{
|
||||||
|
33%{background-size:calc(100%/3) 0% ,calc(100%/3) 100%,calc(100%/3) 100%}
|
||||||
|
50%{background-size:calc(100%/3) 100%,calc(100%/3) 0 ,calc(100%/3) 100%}
|
||||||
|
66%{background-size:calc(100%/3) 100%,calc(100%/3) 100%,calc(100%/3) 0 }
|
||||||
|
}
|
||||||
|
|||||||
@@ -99,14 +99,18 @@
|
|||||||
{% if hint.finding_player == player %}
|
{% if hint.finding_player == player %}
|
||||||
<b>{{ player_names_with_alias[(team, hint.finding_player)] }}</b>
|
<b>{{ player_names_with_alias[(team, hint.finding_player)] }}</b>
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ player_names_with_alias[(team, hint.finding_player)] }}
|
<a href="{{ url_for("get_player_tracker", tracker=room.tracker, tracked_team=team, tracked_player=hint.finding_player) }}">
|
||||||
|
{{ player_names_with_alias[(team, hint.finding_player)] }}
|
||||||
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{% if hint.receiving_player == player %}
|
{% if hint.receiving_player == player %}
|
||||||
<b>{{ player_names_with_alias[(team, hint.receiving_player)] }}</b>
|
<b>{{ player_names_with_alias[(team, hint.receiving_player)] }}</b>
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ player_names_with_alias[(team, hint.receiving_player)] }}
|
<a href="{{ url_for("get_player_tracker", tracker=room.tracker, tracked_team=team, tracked_player=hint.receiving_player) }}">
|
||||||
|
{{ player_names_with_alias[(team, hint.receiving_player)] }}
|
||||||
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td>{{ item_id_to_name[games[(team, hint.receiving_player)]][hint.item] }}</td>
|
<td>{{ item_id_to_name[games[(team, hint.receiving_player)]][hint.item] }}</td>
|
||||||
|
|||||||
@@ -19,28 +19,30 @@
|
|||||||
{% block body %}
|
{% block body %}
|
||||||
{% include 'header/grassHeader.html' %}
|
{% include 'header/grassHeader.html' %}
|
||||||
<div id="host-room">
|
<div id="host-room">
|
||||||
{% if room.owner == session["_id"] %}
|
<span id="host-room-info">
|
||||||
Room created from <a href="{{ url_for("view_seed", seed=room.seed.id) }}">Seed #{{ room.seed.id|suuid }}</a>
|
{% if room.owner == session["_id"] %}
|
||||||
<br />
|
Room created from <a href="{{ url_for("view_seed", seed=room.seed.id) }}">Seed #{{ room.seed.id|suuid }}</a>
|
||||||
{% endif %}
|
<br />
|
||||||
{% if room.tracker %}
|
{% endif %}
|
||||||
This room has a <a href="{{ url_for("get_multiworld_tracker", tracker=room.tracker) }}">Multiworld Tracker</a>
|
{% if room.tracker %}
|
||||||
and a <a href="{{ url_for("get_multiworld_sphere_tracker", tracker=room.tracker) }}">Sphere Tracker</a> enabled.
|
This room has a <a href="{{ url_for("get_multiworld_tracker", tracker=room.tracker) }}">Multiworld Tracker</a>
|
||||||
<br />
|
and a <a href="{{ url_for("get_multiworld_sphere_tracker", tracker=room.tracker) }}">Sphere Tracker</a> enabled.
|
||||||
{% endif %}
|
<br />
|
||||||
The server for this room will be paused after {{ room.timeout//60//60 }} hours of inactivity.
|
{% endif %}
|
||||||
Should you wish to continue later,
|
The server for this room will be paused after {{ room.timeout//60//60 }} hours of inactivity.
|
||||||
anyone can simply refresh this page and the server will resume.<br>
|
Should you wish to continue later,
|
||||||
{% if room.last_port == -1 %}
|
anyone can simply refresh this page and the server will resume.<br>
|
||||||
There was an error hosting this Room. Another attempt will be made on refreshing this page.
|
{% if room.last_port == -1 %}
|
||||||
The most likely failure reason is that the multiworld is too old to be loaded now.
|
There was an error hosting this Room. Another attempt will be made on refreshing this page.
|
||||||
{% elif room.last_port %}
|
The most likely failure reason is that the multiworld is too old to be loaded now.
|
||||||
You can connect to this room by using <span class="interactive"
|
{% elif room.last_port %}
|
||||||
data-tooltip="This means address/ip is {{ config['HOST_ADDRESS'] }} and port is {{ room.last_port }}.">
|
You can connect to this room by using <span class="interactive"
|
||||||
'/connect {{ config['HOST_ADDRESS'] }}:{{ room.last_port }}'
|
data-tooltip="This means address/ip is {{ config['HOST_ADDRESS'] }} and port is {{ room.last_port }}.">
|
||||||
</span>
|
'/connect {{ config['HOST_ADDRESS'] }}:{{ room.last_port }}'
|
||||||
in the <a href="{{ url_for("tutorial_landing")}}">client</a>.<br>
|
</span>
|
||||||
{% endif %}
|
in the <a href="{{ url_for("tutorial_landing")}}">client</a>.<br>
|
||||||
|
{% endif %}
|
||||||
|
</span>
|
||||||
{{ macros.list_patches_room(room) }}
|
{{ macros.list_patches_room(room) }}
|
||||||
{% if room.owner == session["_id"] %}
|
{% if room.owner == session["_id"] %}
|
||||||
<div style="display: flex; align-items: center;">
|
<div style="display: flex; align-items: center;">
|
||||||
@@ -49,6 +51,7 @@
|
|||||||
<label for="cmd"></label>
|
<label for="cmd"></label>
|
||||||
<input class="form-control" type="text" id="cmd" name="cmd"
|
<input class="form-control" type="text" id="cmd" name="cmd"
|
||||||
placeholder="Server Command. /help to list them, list gets appended to log.">
|
placeholder="Server Command. /help to list them, list gets appended to log.">
|
||||||
|
<span class="loader"></span>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
<a href="{{ url_for("display_log", room=room.id) }}">
|
<a href="{{ url_for("display_log", room=room.id) }}">
|
||||||
@@ -62,6 +65,7 @@
|
|||||||
let url = '{{ url_for('display_log', room = room.id) }}';
|
let url = '{{ url_for('display_log', room = room.id) }}';
|
||||||
let bytesReceived = {{ log_len }};
|
let bytesReceived = {{ log_len }};
|
||||||
let updateLogTimeout;
|
let updateLogTimeout;
|
||||||
|
let updateLogImmediately = false;
|
||||||
let awaitingCommandResponse = false;
|
let awaitingCommandResponse = false;
|
||||||
let logger = document.getElementById("logger");
|
let logger = document.getElementById("logger");
|
||||||
|
|
||||||
@@ -78,29 +82,36 @@
|
|||||||
|
|
||||||
async function updateLog() {
|
async function updateLog() {
|
||||||
try {
|
try {
|
||||||
let res = await fetch(url, {
|
if (!document.hidden) {
|
||||||
headers: {
|
updateLogImmediately = false;
|
||||||
'Range': `bytes=${bytesReceived}-`,
|
let res = await fetch(url, {
|
||||||
}
|
headers: {
|
||||||
});
|
'Range': `bytes=${bytesReceived}-`,
|
||||||
if (res.ok) {
|
}
|
||||||
let text = await res.text();
|
});
|
||||||
if (text.length > 0) {
|
if (res.ok) {
|
||||||
awaitingCommandResponse = false;
|
let text = await res.text();
|
||||||
if (bytesReceived === 0 || res.status !== 206) {
|
if (text.length > 0) {
|
||||||
logger.innerHTML = '';
|
awaitingCommandResponse = false;
|
||||||
}
|
if (bytesReceived === 0 || res.status !== 206) {
|
||||||
if (res.status !== 206) {
|
logger.innerHTML = '';
|
||||||
bytesReceived = 0;
|
}
|
||||||
} else {
|
if (res.status !== 206) {
|
||||||
bytesReceived += new Blob([text]).size;
|
bytesReceived = 0;
|
||||||
}
|
} else {
|
||||||
if (logger.innerHTML.endsWith('…')) {
|
bytesReceived += new Blob([text]).size;
|
||||||
logger.innerHTML = logger.innerHTML.substring(0, logger.innerHTML.length - 1);
|
}
|
||||||
}
|
if (logger.innerHTML.endsWith('…')) {
|
||||||
logger.appendChild(document.createTextNode(text));
|
logger.innerHTML = logger.innerHTML.substring(0, logger.innerHTML.length - 1);
|
||||||
scrollToBottom(logger);
|
}
|
||||||
|
logger.appendChild(document.createTextNode(text));
|
||||||
|
scrollToBottom(logger);
|
||||||
|
let loader = document.getElementById("command-form").getElementsByClassName("loader")[0];
|
||||||
|
loader.classList.remove("loading");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
updateLogImmediately = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
@@ -125,20 +136,62 @@
|
|||||||
});
|
});
|
||||||
ev.preventDefault(); // has to happen before first await
|
ev.preventDefault(); // has to happen before first await
|
||||||
form.reset();
|
form.reset();
|
||||||
let res = await req;
|
let loader = form.getElementsByClassName("loader")[0];
|
||||||
if (res.ok || res.type === 'opaqueredirect') {
|
loader.classList.add("loading");
|
||||||
awaitingCommandResponse = true;
|
try {
|
||||||
window.clearTimeout(updateLogTimeout);
|
let res = await req;
|
||||||
updateLogTimeout = window.setTimeout(updateLog, 100);
|
if (res.ok || res.type === 'opaqueredirect') {
|
||||||
} else {
|
awaitingCommandResponse = true;
|
||||||
window.alert(res.statusText);
|
window.clearTimeout(updateLogTimeout);
|
||||||
|
updateLogTimeout = window.setTimeout(updateLog, 100);
|
||||||
|
} else {
|
||||||
|
loader.classList.remove("loading");
|
||||||
|
window.alert(res.statusText);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
loader.classList.remove("loading");
|
||||||
|
window.alert(e.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById("command-form").addEventListener("submit", postForm);
|
document.getElementById("command-form").addEventListener("submit", postForm);
|
||||||
updateLogTimeout = window.setTimeout(updateLog, 1000);
|
updateLogTimeout = window.setTimeout(updateLog, 1000);
|
||||||
logger.scrollTop = logger.scrollHeight;
|
logger.scrollTop = logger.scrollHeight;
|
||||||
|
document.addEventListener("visibilitychange", () => {
|
||||||
|
if (!document.hidden && updateLogImmediately) {
|
||||||
|
updateLog();
|
||||||
|
}
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
<script>
|
||||||
|
function updateInfo() {
|
||||||
|
let url = new URL(window.location.href);
|
||||||
|
url.search = "?update";
|
||||||
|
fetch(url)
|
||||||
|
.then(res => {
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error(`HTTP error ${res.status}`);
|
||||||
|
}
|
||||||
|
return res.text()
|
||||||
|
})
|
||||||
|
.then(text => new DOMParser().parseFromString(text, 'text/html'))
|
||||||
|
.then(newDocument => {
|
||||||
|
let el = newDocument.getElementById("host-room-info");
|
||||||
|
document.getElementById("host-room-info").innerHTML = el.innerHTML;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.querySelector("meta[http-equiv='refresh']")) {
|
||||||
|
console.log("Refresh!");
|
||||||
|
window.addEventListener('load', function () {
|
||||||
|
for (let i=0; i<3; i++) {
|
||||||
|
window.setTimeout(updateInfo, Math.pow(2, i) * 2000); // 2, 4, 8s
|
||||||
|
}
|
||||||
|
window.stop(); // cancel meta refresh
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
{% for patch in room.seed.slots|list|sort(attribute="player_id") %}
|
{% for patch in room.seed.slots|list|sort(attribute="player_id") %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ patch.player_id }}</td>
|
<td>{{ patch.player_id }}</td>
|
||||||
<td data-tooltip="Connect via TextClient"><a href="archipelago://{{ patch.player_name | e}}:None@{{ config['HOST_ADDRESS'] }}:{{ room.last_port }}">{{ patch.player_name }}</a></td>
|
<td data-tooltip="Connect via Game Client"><a href="archipelago://{{ patch.player_name | e}}:None@{{ config['HOST_ADDRESS'] }}:{{ room.last_port }}?game={{ patch.game }}&room={{ room.id | suuid }}">{{ patch.player_name }}</a></td>
|
||||||
<td>{{ patch.game }}</td>
|
<td>{{ patch.game }}</td>
|
||||||
<td>
|
<td>
|
||||||
{% if patch.data %}
|
{% if patch.data %}
|
||||||
|
|||||||
@@ -1,5 +1,21 @@
|
|||||||
{% extends 'tablepage.html' %}
|
{% extends 'tablepage.html' %}
|
||||||
|
|
||||||
|
{%- macro games(slots) -%}
|
||||||
|
{%- set gameList = [] -%}
|
||||||
|
{%- set maxGamesToShow = 10 -%}
|
||||||
|
|
||||||
|
{%- for slot in (slots|list|sort(attribute="player_id"))[:maxGamesToShow] -%}
|
||||||
|
{% set player = "#" + slot["player_id"]|string + " " + slot["player_name"] + " : " + slot["game"] -%}
|
||||||
|
{% set _ = gameList.append(player) -%}
|
||||||
|
{%- endfor -%}
|
||||||
|
|
||||||
|
{%- if slots|length > maxGamesToShow -%}
|
||||||
|
{% set _ = gameList.append("... and " + (slots|length - maxGamesToShow)|string + " more") -%}
|
||||||
|
{%- endif -%}
|
||||||
|
|
||||||
|
{{ gameList|join('\n') }}
|
||||||
|
{%- endmacro -%}
|
||||||
|
|
||||||
{% block head %}
|
{% block head %}
|
||||||
{{ super() }}
|
{{ super() }}
|
||||||
<title>User Content</title>
|
<title>User Content</title>
|
||||||
@@ -33,10 +49,12 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td><a href="{{ url_for("view_seed", seed=room.seed.id) }}">{{ room.seed.id|suuid }}</a></td>
|
<td><a href="{{ url_for("view_seed", seed=room.seed.id) }}">{{ room.seed.id|suuid }}</a></td>
|
||||||
<td><a href="{{ url_for("host_room", room=room.id) }}">{{ room.id|suuid }}</a></td>
|
<td><a href="{{ url_for("host_room", room=room.id) }}">{{ room.id|suuid }}</a></td>
|
||||||
<td>{{ room.seed.slots|length }}</td>
|
<td title="{{ games(room.seed.slots) }}">
|
||||||
|
{{ room.seed.slots|length }}
|
||||||
|
</td>
|
||||||
<td>{{ room.creation_time.strftime("%Y-%m-%d %H:%M") }}</td>
|
<td>{{ room.creation_time.strftime("%Y-%m-%d %H:%M") }}</td>
|
||||||
<td>{{ room.last_activity.strftime("%Y-%m-%d %H:%M") }}</td>
|
<td>{{ room.last_activity.strftime("%Y-%m-%d %H:%M") }}</td>
|
||||||
<td><a href="{{ url_for("disown_room", room=room.id) }}">Delete next maintenance.</td>
|
<td><a href="{{ url_for("disown_room", room=room.id) }}">Delete next maintenance.</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
@@ -60,16 +78,21 @@
|
|||||||
{% for seed in seeds %}
|
{% for seed in seeds %}
|
||||||
<tr>
|
<tr>
|
||||||
<td><a href="{{ url_for("view_seed", seed=seed.id) }}">{{ seed.id|suuid }}</a></td>
|
<td><a href="{{ url_for("view_seed", seed=seed.id) }}">{{ seed.id|suuid }}</a></td>
|
||||||
<td>{% if seed.multidata %}{{ seed.slots|length }}{% else %}1{% endif %}
|
<td title="{{ games(seed.slots) }}">
|
||||||
|
{% if seed.multidata %}
|
||||||
|
{{ seed.slots|length }}
|
||||||
|
{% else %}
|
||||||
|
1
|
||||||
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td>{{ seed.creation_time.strftime("%Y-%m-%d %H:%M") }}</td>
|
<td>{{ seed.creation_time.strftime("%Y-%m-%d %H:%M") }}</td>
|
||||||
<td><a href="{{ url_for("disown_seed", seed=seed.id) }}">Delete next maintenance.</td>
|
<td><a href="{{ url_for("disown_seed", seed=seed.id) }}">Delete next maintenance.</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
{% else %}
|
{% else %}
|
||||||
You have no generated any seeds yet!
|
You have not generated any seeds yet!
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -46,7 +46,7 @@
|
|||||||
/worlds/clique/ @ThePhar
|
/worlds/clique/ @ThePhar
|
||||||
|
|
||||||
# Dark Souls III
|
# Dark Souls III
|
||||||
/worlds/dark_souls_3/ @Marechal-L
|
/worlds/dark_souls_3/ @Marechal-L @nex3
|
||||||
|
|
||||||
# Donkey Kong Country 3
|
# Donkey Kong Country 3
|
||||||
/worlds/dkc3/ @PoryGone
|
/worlds/dkc3/ @PoryGone
|
||||||
@@ -118,9 +118,6 @@
|
|||||||
# Noita
|
# Noita
|
||||||
/worlds/noita/ @ScipioWright @heinermann
|
/worlds/noita/ @ScipioWright @heinermann
|
||||||
|
|
||||||
# Ocarina of Time
|
|
||||||
/worlds/oot/ @espeon65536
|
|
||||||
|
|
||||||
# Old School Runescape
|
# Old School Runescape
|
||||||
/worlds/osrs @digiholic
|
/worlds/osrs @digiholic
|
||||||
|
|
||||||
@@ -230,6 +227,9 @@
|
|||||||
# Links Awakening DX
|
# Links Awakening DX
|
||||||
# /worlds/ladx/
|
# /worlds/ladx/
|
||||||
|
|
||||||
|
# Ocarina of Time
|
||||||
|
# /worlds/oot/
|
||||||
|
|
||||||
## Disabled Unmaintained Worlds
|
## Disabled Unmaintained Worlds
|
||||||
|
|
||||||
# The following worlds in this repo are currently unmaintained and disabled as they do not work in core. If you are
|
# The following worlds in this repo are currently unmaintained and disabled as they do not work in core. If you are
|
||||||
|
|||||||
@@ -395,6 +395,7 @@ Some special keys exist with specific return data, all of them have the prefix `
|
|||||||
| item_name_groups_{game_name} | dict\[str, list\[str\]\] | item_name_groups belonging to the requested game. |
|
| item_name_groups_{game_name} | dict\[str, list\[str\]\] | item_name_groups belonging to the requested game. |
|
||||||
| location_name_groups_{game_name} | dict\[str, list\[str\]\] | location_name_groups belonging to the requested game. |
|
| location_name_groups_{game_name} | dict\[str, list\[str\]\] | location_name_groups belonging to the requested game. |
|
||||||
| client_status_{team}_{slot} | [ClientStatus](#ClientStatus) | The current game status of the requested player. |
|
| client_status_{team}_{slot} | [ClientStatus](#ClientStatus) | The current game status of the requested player. |
|
||||||
|
| race_mode | int | 0 if race mode is disabled, and 1 if it's enabled. |
|
||||||
|
|
||||||
### Set
|
### Set
|
||||||
Used to write data to the server's data storage, that data can then be shared across worlds or just saved for later. Values for keys in the data storage can be retrieved with a [Get](#Get) package, or monitored with a [SetNotify](#SetNotify) package.
|
Used to write data to the server's data storage, that data can then be shared across worlds or just saved for later. Values for keys in the data storage can be retrieved with a [Get](#Get) package, or monitored with a [SetNotify](#SetNotify) package.
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ display as `Value1` on the webhost.
|
|||||||
files, and both will resolve as `value1`. This should be used when changing options around, i.e. changing a Toggle to a
|
files, and both will resolve as `value1`. This should be used when changing options around, i.e. changing a Toggle to a
|
||||||
Choice, and defining `alias_true = option_full`.
|
Choice, and defining `alias_true = option_full`.
|
||||||
- All options with a fixed set of possible values (i.e. those which inherit from `Toggle`, `(Text)Choice` or
|
- All options with a fixed set of possible values (i.e. those which inherit from `Toggle`, `(Text)Choice` or
|
||||||
`(Named/Special)Range`) support `random` as a generic option. `random` chooses from any of the available values for that
|
`(Named)Range`) support `random` as a generic option. `random` chooses from any of the available values for that
|
||||||
option, and is reserved by AP. You can set this as your default value, but you cannot define your own `option_random`.
|
option, and is reserved by AP. You can set this as your default value, but you cannot define your own `option_random`.
|
||||||
However, you can override `from_text` and handle `text == "random"` to customize its behavior or
|
However, you can override `from_text` and handle `text == "random"` to customize its behavior or
|
||||||
implement it for additional option types.
|
implement it for additional option types.
|
||||||
@@ -129,6 +129,23 @@ class Difficulty(Choice):
|
|||||||
default = 1
|
default = 1
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Option Visibility
|
||||||
|
Every option has a Visibility IntFlag, defaulting to `all` (`0b1111`). This lets you choose where the option will be
|
||||||
|
displayed. This only impacts where options are displayed, not how they can be used. Hidden options are still valid
|
||||||
|
options in a yaml. The flags are as follows:
|
||||||
|
* `none` (`0b0000`): This option is not shown anywhere
|
||||||
|
* `template` (`0b0001`): This option shows up in template yamls
|
||||||
|
* `simple_ui` (`0b0010`): This option shows up on the options page
|
||||||
|
* `complex_ui` (`0b0100`): This option shows up on the advanced/weighted options page
|
||||||
|
* `spoiler` (`0b1000`): This option shows up in spoiler logs
|
||||||
|
|
||||||
|
```python
|
||||||
|
from Options import Choice, Visibility
|
||||||
|
|
||||||
|
class HiddenChoiceOption(Choice):
|
||||||
|
visibility = Visibility.none
|
||||||
|
```
|
||||||
|
|
||||||
### Option Groups
|
### Option Groups
|
||||||
Options may be categorized into groups for display on the WebHost. Option groups are displayed in the order specified
|
Options may be categorized into groups for display on the WebHost. Option groups are displayed in the order specified
|
||||||
by your world on the player-options and weighted-options pages. In the generated template files, there will be a comment
|
by your world on the player-options and weighted-options pages. In the generated template files, there will be a comment
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ Recommended steps
|
|||||||
* Refer to [Windows Compilers on the python wiki](https://wiki.python.org/moin/WindowsCompilers) for details.
|
* Refer to [Windows Compilers on the python wiki](https://wiki.python.org/moin/WindowsCompilers) for details.
|
||||||
Generally, selecting the box for "Desktop Development with C++" will provide what you need.
|
Generally, selecting the box for "Desktop Development with C++" will provide what you need.
|
||||||
* Build tools are not required if all modules are installed pre-compiled. Pre-compiled modules are pinned on
|
* Build tools are not required if all modules are installed pre-compiled. Pre-compiled modules are pinned on
|
||||||
[Discord in #archipelago-dev](https://discord.com/channels/731205301247803413/731214280439103580/905154456377757808)
|
[Discord in #ap-core-dev](https://discord.com/channels/731205301247803413/731214280439103580/905154456377757808)
|
||||||
|
|
||||||
* It is recommended to use [PyCharm IDE](https://www.jetbrains.com/pycharm/)
|
* It is recommended to use [PyCharm IDE](https://www.jetbrains.com/pycharm/)
|
||||||
* Run Generate.py which will prompt installation of missing modules, press enter to confirm
|
* Run Generate.py which will prompt installation of missing modules, press enter to confirm
|
||||||
|
|||||||
@@ -288,8 +288,8 @@ like entrance randomization in logic.
|
|||||||
|
|
||||||
Regions have a list called `exits`, containing `Entrance` objects representing transitions to other regions.
|
Regions have a list called `exits`, containing `Entrance` objects representing transitions to other regions.
|
||||||
|
|
||||||
There must be one special region (Called "Menu" by default, but configurable using [origin_region_name](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/AutoWorld.py#L295-L296)),
|
There must be one special region, "Menu", from which the logic unfolds. AP assumes that a player will always be able to
|
||||||
from which the logic unfolds. AP assumes that a player will always be able to return to this starting region by resetting the game ("Save and quit").
|
return to the "Menu" region by resetting the game ("Save and quit").
|
||||||
|
|
||||||
### Entrances
|
### Entrances
|
||||||
|
|
||||||
@@ -328,9 +328,6 @@ Even doing `state.can_reach_location` or `state.can_reach_entrance` is problemat
|
|||||||
You can use `multiworld.register_indirect_condition(region, entrance)` to explicitly tell the generator that, when a given region becomes accessible, it is necessary to re-check a specific entrance.
|
You can use `multiworld.register_indirect_condition(region, entrance)` to explicitly tell the generator that, when a given region becomes accessible, it is necessary to re-check a specific entrance.
|
||||||
You **must** use `multiworld.register_indirect_condition` if you perform this kind of `can_reach` from an entrance access rule, unless you have a **very** good technical understanding of the relevant code and can reason why it will never lead to problems in your case.
|
You **must** use `multiworld.register_indirect_condition` if you perform this kind of `can_reach` from an entrance access rule, unless you have a **very** good technical understanding of the relevant code and can reason why it will never lead to problems in your case.
|
||||||
|
|
||||||
Alternatively, you can set [world.explicit_indirect_conditions = False](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/AutoWorld.py#L298-L301),
|
|
||||||
avoiding the need for indirect conditions at the expense of performance.
|
|
||||||
|
|
||||||
### Item Rules
|
### Item Rules
|
||||||
|
|
||||||
An item rule is a function that returns `True` or `False` for a `Location` based on a single item. It can be used to
|
An item rule is a function that returns `True` or `False` for a `Location` based on a single item. It can be used to
|
||||||
@@ -466,7 +463,7 @@ The world has to provide the following things for generation:
|
|||||||
|
|
||||||
* the properties mentioned above
|
* the properties mentioned above
|
||||||
* additions to the item pool
|
* additions to the item pool
|
||||||
* additions to the regions list: at least one named after the world class's origin_region_name ("Menu" by default)
|
* additions to the regions list: at least one called "Menu"
|
||||||
* locations placed inside those regions
|
* locations placed inside those regions
|
||||||
* a `def create_item(self, item: str) -> MyGameItem` to create any item on demand
|
* a `def create_item(self, item: str) -> MyGameItem` to create any item on demand
|
||||||
* applying `self.multiworld.push_precollected` for world-defined start inventory
|
* applying `self.multiworld.push_precollected` for world-defined start inventory
|
||||||
@@ -519,7 +516,7 @@ def generate_early(self) -> None:
|
|||||||
|
|
||||||
```python
|
```python
|
||||||
def create_regions(self) -> None:
|
def create_regions(self) -> None:
|
||||||
# Add regions to the multiworld. One of them must use the origin_region_name as its name ("Menu" by default).
|
# Add regions to the multiworld. "Menu" is the required starting point.
|
||||||
# Arguments to Region() are name, player, multiworld, and optionally hint_text
|
# Arguments to Region() are name, player, multiworld, and optionally hint_text
|
||||||
menu_region = Region("Menu", self.player, self.multiworld)
|
menu_region = Region("Menu", self.player, self.multiworld)
|
||||||
self.multiworld.regions.append(menu_region) # or use += [menu_region...]
|
self.multiworld.regions.append(menu_region) # or use += [menu_region...]
|
||||||
|
|||||||
@@ -26,8 +26,17 @@ Unless these are shared between multiple people, we expect the following from ea
|
|||||||
### Adding a World
|
### Adding a World
|
||||||
|
|
||||||
When we merge your world into the core Archipelago repository, you automatically become world maintainer unless you
|
When we merge your world into the core Archipelago repository, you automatically become world maintainer unless you
|
||||||
nominate someone else (i.e. there are multiple devs). You can define who is allowed to approve changes to your world
|
nominate someone else (i.e. there are multiple devs).
|
||||||
in the [CODEOWNERS](/docs/CODEOWNERS) document.
|
|
||||||
|
### Being added as a maintainer to an existing implementation
|
||||||
|
|
||||||
|
At any point, a world maintainer can approve the addition of another maintainer to their world.
|
||||||
|
In order to do this, either an existing maintainer or the new maintainer must open a PR updating the
|
||||||
|
[CODEOWNERS](/docs/CODEOWNERS) file.
|
||||||
|
This change must be approved by all existing maintainers of the affected world, the new maintainer candidate, and
|
||||||
|
one core maintainer.
|
||||||
|
To help the core team review the change, information about the new maintainer and their contributions should be
|
||||||
|
included in the PR description.
|
||||||
|
|
||||||
### Getting Voted
|
### Getting Voted
|
||||||
|
|
||||||
@@ -35,7 +44,7 @@ When a world is unmaintained, the [core maintainers](https://github.com/orgs/Arc
|
|||||||
can vote for a new maintainer if there is a candidate.
|
can vote for a new maintainer if there is a candidate.
|
||||||
For a vote to pass, the majority of participating core maintainers must vote in the affirmative.
|
For a vote to pass, the majority of participating core maintainers must vote in the affirmative.
|
||||||
The time limit is 1 week, but can end early if the majority is reached earlier.
|
The time limit is 1 week, but can end early if the majority is reached earlier.
|
||||||
Voting shall be conducted on Discord in #archipelago-dev.
|
Voting shall be conducted on Discord in #ap-core-dev.
|
||||||
|
|
||||||
## Dropping out
|
## Dropping out
|
||||||
|
|
||||||
@@ -51,7 +60,7 @@ for example when they become unreachable.
|
|||||||
For a vote to pass, the majority of participating core maintainers must vote in the affirmative.
|
For a vote to pass, the majority of participating core maintainers must vote in the affirmative.
|
||||||
The time limit is 2 weeks, but can end early if the majority is reached earlier AND the world maintainer was pinged and
|
The time limit is 2 weeks, but can end early if the majority is reached earlier AND the world maintainer was pinged and
|
||||||
made their case or was pinged and has been unreachable for more than 2 weeks already.
|
made their case or was pinged and has been unreachable for more than 2 weeks already.
|
||||||
Voting shall be conducted on Discord in #archipelago-dev. Commits that are a direct result of the voting shall include
|
Voting shall be conducted on Discord in #ap-core-dev. Commits that are a direct result of the voting shall include
|
||||||
date, voting members and final result in the commit message.
|
date, voting members and final result in the commit message.
|
||||||
|
|
||||||
## Handling of Unmaintained Worlds
|
## Handling of Unmaintained Worlds
|
||||||
|
|||||||
@@ -228,8 +228,8 @@ Root: HKCR; Subkey: "{#MyAppName}worlddata\shell\open\command"; ValueData: """{a
|
|||||||
|
|
||||||
Root: HKCR; Subkey: "archipelago"; ValueType: "string"; ValueData: "Archipegalo Protocol"; Flags: uninsdeletekey;
|
Root: HKCR; Subkey: "archipelago"; ValueType: "string"; ValueData: "Archipegalo Protocol"; Flags: uninsdeletekey;
|
||||||
Root: HKCR; Subkey: "archipelago"; ValueType: "string"; ValueName: "URL Protocol"; ValueData: "";
|
Root: HKCR; Subkey: "archipelago"; ValueType: "string"; ValueName: "URL Protocol"; ValueData: "";
|
||||||
Root: HKCR; Subkey: "archipelago\DefaultIcon"; ValueType: "string"; ValueData: "{app}\ArchipelagoTextClient.exe,0";
|
Root: HKCR; Subkey: "archipelago\DefaultIcon"; ValueType: "string"; ValueData: "{app}\ArchipelagoLauncher.exe,0";
|
||||||
Root: HKCR; Subkey: "archipelago\shell\open\command"; ValueType: "string"; ValueData: """{app}\ArchipelagoTextClient.exe"" ""%1""";
|
Root: HKCR; Subkey: "archipelago\shell\open\command"; ValueType: "string"; ValueData: """{app}\ArchipelagoLauncher.exe"" ""%1""";
|
||||||
|
|
||||||
[Code]
|
[Code]
|
||||||
// See: https://stackoverflow.com/a/51614652/2287576
|
// See: https://stackoverflow.com/a/51614652/2287576
|
||||||
|
|||||||
16
kvui.py
16
kvui.py
@@ -243,6 +243,9 @@ class ServerLabel(HovererableLabel):
|
|||||||
f"\nYou currently have {ctx.hint_points} points."
|
f"\nYou currently have {ctx.hint_points} points."
|
||||||
elif ctx.hint_cost == 0:
|
elif ctx.hint_cost == 0:
|
||||||
text += "\n!hint is free to use."
|
text += "\n!hint is free to use."
|
||||||
|
if ctx.stored_data and "_read_race_mode" in ctx.stored_data:
|
||||||
|
text += "\nRace mode is enabled." \
|
||||||
|
if ctx.stored_data["_read_race_mode"] else "\nRace mode is disabled."
|
||||||
else:
|
else:
|
||||||
text += f"\nYou are not authenticated yet."
|
text += f"\nYou are not authenticated yet."
|
||||||
|
|
||||||
@@ -536,9 +539,8 @@ class GameManager(App):
|
|||||||
# show Archipelago tab if other logging is present
|
# show Archipelago tab if other logging is present
|
||||||
self.tabs.add_widget(panel)
|
self.tabs.add_widget(panel)
|
||||||
|
|
||||||
hint_panel = TabbedPanelItem(text="Hints")
|
hint_panel = self.add_client_tab("Hints", HintLog(self.json_to_kivy_parser))
|
||||||
self.log_panels["Hints"] = hint_panel.content = HintLog(self.json_to_kivy_parser)
|
self.log_panels["Hints"] = hint_panel.content
|
||||||
self.tabs.add_widget(hint_panel)
|
|
||||||
|
|
||||||
if len(self.logging_pairs) == 1:
|
if len(self.logging_pairs) == 1:
|
||||||
self.tabs.default_tab_text = "Archipelago"
|
self.tabs.default_tab_text = "Archipelago"
|
||||||
@@ -572,6 +574,14 @@ class GameManager(App):
|
|||||||
|
|
||||||
return self.container
|
return self.container
|
||||||
|
|
||||||
|
def add_client_tab(self, title: str, content: Widget) -> Widget:
|
||||||
|
"""Adds a new tab to the client window with a given title, and provides a given Widget as its content.
|
||||||
|
Returns the new tab widget, with the provided content being placed on the tab as content."""
|
||||||
|
new_tab = TabbedPanelItem(text=title)
|
||||||
|
new_tab.content = content
|
||||||
|
self.tabs.add_widget(new_tab)
|
||||||
|
return new_tab
|
||||||
|
|
||||||
def update_texts(self, dt):
|
def update_texts(self, dt):
|
||||||
if hasattr(self.tabs.content.children[0], "fix_heights"):
|
if hasattr(self.tabs.content.children[0], "fix_heights"):
|
||||||
self.tabs.content.children[0].fix_heights() # TODO: remove this when Kivy fixes this upstream
|
self.tabs.content.children[0].fix_heights() # TODO: remove this when Kivy fixes this upstream
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ class TestTwoPlayerMulti(MultiworldTestBase):
|
|||||||
for world in self.multiworld.worlds.values():
|
for world in self.multiworld.worlds.values():
|
||||||
world.options.accessibility.value = Accessibility.option_full
|
world.options.accessibility.value = Accessibility.option_full
|
||||||
self.assertSteps(gen_steps)
|
self.assertSteps(gen_steps)
|
||||||
with self.subTest("filling multiworld", seed=self.multiworld.seed):
|
with self.subTest("filling multiworld", games=world_type.game, seed=self.multiworld.seed):
|
||||||
distribute_items_restrictive(self.multiworld)
|
distribute_items_restrictive(self.multiworld)
|
||||||
call_all(self.multiworld, "post_fill")
|
call_all(self.multiworld, "post_fill")
|
||||||
self.assertTrue(self.fulfills_accessibility(), "Collected all locations, but can't beat the game")
|
self.assertTrue(self.fulfills_accessibility(), "Collected all locations, but can't beat the game")
|
||||||
|
|||||||
73
test/webhost/test_generate.py
Normal file
73
test/webhost/test_generate.py
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import zipfile
|
||||||
|
from io import BytesIO
|
||||||
|
|
||||||
|
from flask import url_for
|
||||||
|
|
||||||
|
from . import TestBase
|
||||||
|
|
||||||
|
|
||||||
|
class TestGenerate(TestBase):
|
||||||
|
def test_valid_yaml(self) -> None:
|
||||||
|
"""
|
||||||
|
Verify that posting a valid yaml will start generating a game.
|
||||||
|
"""
|
||||||
|
with self.app.app_context(), self.app.test_request_context():
|
||||||
|
yaml_data = """
|
||||||
|
name: Player1
|
||||||
|
game: Archipelago
|
||||||
|
Archipelago: {}
|
||||||
|
"""
|
||||||
|
response = self.client.post(url_for("generate"),
|
||||||
|
data={"file": (BytesIO(yaml_data.encode("utf-8")), "test.yaml")},
|
||||||
|
follow_redirects=True)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertTrue("/seed/" in response.request.path or
|
||||||
|
"/wait/" in response.request.path,
|
||||||
|
f"Response did not properly redirect ({response.request.path})")
|
||||||
|
|
||||||
|
def test_empty_zip(self) -> None:
|
||||||
|
"""
|
||||||
|
Verify that posting an empty zip will give an error.
|
||||||
|
"""
|
||||||
|
with self.app.app_context(), self.app.test_request_context():
|
||||||
|
zip_data = BytesIO()
|
||||||
|
zipfile.ZipFile(zip_data, "w").close()
|
||||||
|
zip_data.seek(0)
|
||||||
|
self.assertGreater(len(zip_data.read()), 0)
|
||||||
|
zip_data.seek(0)
|
||||||
|
response = self.client.post(url_for("generate"),
|
||||||
|
data={"file": (zip_data, "test.zip")},
|
||||||
|
follow_redirects=True)
|
||||||
|
self.assertIn("user-message", response.text,
|
||||||
|
"Request did not call flash()")
|
||||||
|
self.assertIn("not find any valid files", response.text,
|
||||||
|
"Response shows unexpected error")
|
||||||
|
self.assertIn("generate-game-form", response.text,
|
||||||
|
"Response did not get user back to the form")
|
||||||
|
|
||||||
|
def test_too_many_players(self) -> None:
|
||||||
|
"""
|
||||||
|
Verify that posting too many players will give an error.
|
||||||
|
"""
|
||||||
|
max_roll = self.app.config["MAX_ROLL"]
|
||||||
|
# validate that max roll has a sensible value, otherwise we probably changed how it works
|
||||||
|
self.assertIsInstance(max_roll, int)
|
||||||
|
self.assertGreater(max_roll, 1)
|
||||||
|
self.assertLess(max_roll, 100)
|
||||||
|
# create a yaml with max_roll+1 players and watch it fail
|
||||||
|
with self.app.app_context(), self.app.test_request_context():
|
||||||
|
yaml_data = "---\n".join([
|
||||||
|
f"name: Player{n}\n"
|
||||||
|
"game: Archipelago\n"
|
||||||
|
"Archipelago: {}\n"
|
||||||
|
for n in range(1, max_roll + 2)
|
||||||
|
])
|
||||||
|
response = self.client.post(url_for("generate"),
|
||||||
|
data={"file": (BytesIO(yaml_data.encode("utf-8")), "test.yaml")},
|
||||||
|
follow_redirects=True)
|
||||||
|
self.assertIn("user-message", response.text,
|
||||||
|
"Request did not call flash()")
|
||||||
|
self.assertIn("limited to", response.text,
|
||||||
|
"Response shows unexpected error")
|
||||||
|
self.assertIn("generate-game-form", response.text,
|
||||||
|
"Response did not get user back to the form")
|
||||||
@@ -131,7 +131,8 @@ class TestHostFakeRoom(TestBase):
|
|||||||
f.write(text)
|
f.write(text)
|
||||||
|
|
||||||
with self.app.app_context(), self.app.test_request_context():
|
with self.app.app_context(), self.app.test_request_context():
|
||||||
response = self.client.get(url_for("host_room", room=self.room_id))
|
response = self.client.get(url_for("host_room", room=self.room_id),
|
||||||
|
headers={"User-Agent": "Mozilla/5.0"})
|
||||||
response_text = response.get_data(True)
|
response_text = response.get_data(True)
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertIn("href=\"/seed/", response_text)
|
self.assertIn("href=\"/seed/", response_text)
|
||||||
|
|||||||
@@ -342,7 +342,7 @@ class World(metaclass=AutoWorldRegister):
|
|||||||
|
|
||||||
# overridable methods that get called by Main.py, sorted by execution order
|
# overridable methods that get called by Main.py, sorted by execution order
|
||||||
# can also be implemented as a classmethod and called "stage_<original_name>",
|
# can also be implemented as a classmethod and called "stage_<original_name>",
|
||||||
# in that case the MultiWorld object is passed as an argument, and it gets called once for the entire multiworld.
|
# in that case the MultiWorld object is passed as the first argument, and it gets called once for the entire multiworld.
|
||||||
# An example of this can be found in alttp as stage_pre_fill
|
# An example of this can be found in alttp as stage_pre_fill
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|||||||
@@ -26,10 +26,13 @@ class Component:
|
|||||||
cli: bool
|
cli: bool
|
||||||
func: Optional[Callable]
|
func: Optional[Callable]
|
||||||
file_identifier: Optional[Callable[[str], bool]]
|
file_identifier: Optional[Callable[[str], bool]]
|
||||||
|
game_name: Optional[str]
|
||||||
|
supports_uri: Optional[bool]
|
||||||
|
|
||||||
def __init__(self, display_name: str, script_name: Optional[str] = None, frozen_name: Optional[str] = None,
|
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,
|
cli: bool = False, icon: str = 'icon', component_type: Optional[Type] = None,
|
||||||
func: Optional[Callable] = None, file_identifier: Optional[Callable[[str], bool]] = None):
|
func: Optional[Callable] = None, file_identifier: Optional[Callable[[str], bool]] = None,
|
||||||
|
game_name: Optional[str] = None, supports_uri: Optional[bool] = False):
|
||||||
self.display_name = display_name
|
self.display_name = display_name
|
||||||
self.script_name = script_name
|
self.script_name = script_name
|
||||||
self.frozen_name = frozen_name or f'Archipelago{script_name}' if script_name else None
|
self.frozen_name = frozen_name or f'Archipelago{script_name}' if script_name else None
|
||||||
@@ -45,6 +48,8 @@ class Component:
|
|||||||
Type.ADJUSTER if "Adjuster" in display_name else Type.MISC)
|
Type.ADJUSTER if "Adjuster" in display_name else Type.MISC)
|
||||||
self.func = func
|
self.func = func
|
||||||
self.file_identifier = file_identifier
|
self.file_identifier = file_identifier
|
||||||
|
self.game_name = game_name
|
||||||
|
self.supports_uri = supports_uri
|
||||||
|
|
||||||
def handles_file(self, path: str):
|
def handles_file(self, path: str):
|
||||||
return self.file_identifier(path) if self.file_identifier else False
|
return self.file_identifier(path) if self.file_identifier else False
|
||||||
@@ -56,10 +61,10 @@ class Component:
|
|||||||
processes = weakref.WeakSet()
|
processes = weakref.WeakSet()
|
||||||
|
|
||||||
|
|
||||||
def launch_subprocess(func: Callable, name: str = None):
|
def launch_subprocess(func: Callable, name: str = None, args: Tuple[str, ...] = ()) -> None:
|
||||||
global processes
|
global processes
|
||||||
import multiprocessing
|
import multiprocessing
|
||||||
process = multiprocessing.Process(target=func, name=name)
|
process = multiprocessing.Process(target=func, name=name, args=args)
|
||||||
process.start()
|
process.start()
|
||||||
processes.add(process)
|
processes.add(process)
|
||||||
|
|
||||||
@@ -78,9 +83,9 @@ class SuffixIdentifier:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def launch_textclient():
|
def launch_textclient(*args):
|
||||||
import CommonClient
|
import CommonClient
|
||||||
launch_subprocess(CommonClient.run_as_textclient, name="TextClient")
|
launch_subprocess(CommonClient.run_as_textclient, name="TextClient", args=args)
|
||||||
|
|
||||||
|
|
||||||
def _install_apworld(apworld_src: str = "") -> Optional[Tuple[pathlib.Path, pathlib.Path]]:
|
def _install_apworld(apworld_src: str = "") -> Optional[Tuple[pathlib.Path, pathlib.Path]]:
|
||||||
|
|||||||
@@ -223,8 +223,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']}")
|
raise SyncError(f"Expected response of type SET_MESSAGE_INTERVAL_RESPONSE but got {res['type']}")
|
||||||
|
|
||||||
|
|
||||||
async def guarded_read(ctx: BizHawkContext, read_list: typing.List[typing.Tuple[int, int, str]],
|
async def guarded_read(ctx: BizHawkContext, read_list: typing.Sequence[typing.Tuple[int, int, str]],
|
||||||
guard_list: typing.List[typing.Tuple[int, typing.Iterable[int], str]]) -> typing.Optional[typing.List[bytes]]:
|
guard_list: typing.Sequence[typing.Tuple[int, typing.Sequence[int], str]]) -> typing.Optional[typing.List[bytes]]:
|
||||||
"""Reads an array of bytes at 1 or more addresses if and only if every byte in guard_list matches its expected
|
"""Reads an array of bytes at 1 or more addresses if and only if every byte in guard_list matches its expected
|
||||||
value.
|
value.
|
||||||
|
|
||||||
@@ -266,7 +266,7 @@ async def guarded_read(ctx: BizHawkContext, read_list: typing.List[typing.Tuple[
|
|||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
|
||||||
async def read(ctx: BizHawkContext, read_list: typing.List[typing.Tuple[int, int, str]]) -> typing.List[bytes]:
|
async def read(ctx: BizHawkContext, read_list: typing.Sequence[typing.Tuple[int, int, str]]) -> typing.List[bytes]:
|
||||||
"""Reads data at 1 or more addresses.
|
"""Reads data at 1 or more addresses.
|
||||||
|
|
||||||
Items in `read_list` should be organized `(address, size, domain)` where
|
Items in `read_list` should be organized `(address, size, domain)` where
|
||||||
@@ -278,8 +278,8 @@ async def read(ctx: BizHawkContext, read_list: typing.List[typing.Tuple[int, int
|
|||||||
return await guarded_read(ctx, read_list, [])
|
return await guarded_read(ctx, read_list, [])
|
||||||
|
|
||||||
|
|
||||||
async def guarded_write(ctx: BizHawkContext, write_list: typing.List[typing.Tuple[int, typing.Iterable[int], str]],
|
async def guarded_write(ctx: BizHawkContext, write_list: typing.Sequence[typing.Tuple[int, typing.Sequence[int], str]],
|
||||||
guard_list: typing.List[typing.Tuple[int, typing.Iterable[int], str]]) -> bool:
|
guard_list: typing.Sequence[typing.Tuple[int, typing.Sequence[int], str]]) -> bool:
|
||||||
"""Writes data to 1 or more addresses if and only if every byte in guard_list matches its expected value.
|
"""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
|
Items in `write_list` should be organized `(address, value, domain)` where
|
||||||
@@ -316,7 +316,7 @@ async def guarded_write(ctx: BizHawkContext, write_list: typing.List[typing.Tupl
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
async def write(ctx: BizHawkContext, write_list: typing.List[typing.Tuple[int, typing.Iterable[int], str]]) -> None:
|
async def write(ctx: BizHawkContext, write_list: typing.Sequence[typing.Tuple[int, typing.Sequence[int], str]]) -> None:
|
||||||
"""Writes data to 1 or more addresses.
|
"""Writes data to 1 or more addresses.
|
||||||
|
|
||||||
Items in write_list should be organized `(address, value, domain)` where
|
Items in write_list should be organized `(address, value, domain)` where
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
def launch_client(*args) -> None:
|
def launch_client(*args) -> None:
|
||||||
from .context import launch
|
from .context import launch
|
||||||
launch_subprocess(launch, name="BizHawkClient")
|
launch_subprocess(launch, name="BizHawkClient", args=args)
|
||||||
|
|
||||||
|
|
||||||
component = Component("BizHawk Client", "BizHawkClient", component_type=Type.CLIENT, func=launch_client,
|
component = Component("BizHawk Client", "BizHawkClient", component_type=Type.CLIENT, func=launch_client,
|
||||||
|
|||||||
@@ -59,14 +59,10 @@ class BizHawkClientContext(CommonContext):
|
|||||||
self.bizhawk_ctx = BizHawkContext()
|
self.bizhawk_ctx = BizHawkContext()
|
||||||
self.watcher_timeout = 0.5
|
self.watcher_timeout = 0.5
|
||||||
|
|
||||||
def run_gui(self):
|
def make_gui(self):
|
||||||
from kvui import GameManager
|
ui = super().make_gui()
|
||||||
|
ui.base_title = "Archipelago BizHawk Client"
|
||||||
class BizHawkManager(GameManager):
|
return ui
|
||||||
base_title = "Archipelago BizHawk Client"
|
|
||||||
|
|
||||||
self.ui = BizHawkManager(self)
|
|
||||||
self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI")
|
|
||||||
|
|
||||||
def on_package(self, cmd, args):
|
def on_package(self, cmd, args):
|
||||||
if cmd == "Connected":
|
if cmd == "Connected":
|
||||||
@@ -243,11 +239,11 @@ async def _patch_and_run_game(patch_file: str):
|
|||||||
logger.exception(exc)
|
logger.exception(exc)
|
||||||
|
|
||||||
|
|
||||||
def launch() -> None:
|
def launch(*launch_args) -> None:
|
||||||
async def main():
|
async def main():
|
||||||
parser = get_base_parser()
|
parser = get_base_parser()
|
||||||
parser.add_argument("patch_file", default="", type=str, nargs="?", help="Path to an Archipelago patch file")
|
parser.add_argument("patch_file", default="", type=str, nargs="?", help="Path to an Archipelago patch file")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args(launch_args)
|
||||||
|
|
||||||
ctx = BizHawkClientContext(args.connect, args.password)
|
ctx = BizHawkClientContext(args.connect, args.password)
|
||||||
ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop")
|
ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop")
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import websockets
|
|||||||
import functools
|
import functools
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from typing import List, Any, Iterable
|
from typing import List, Any, Iterable
|
||||||
from NetUtils import decode, encode, JSONtoTextParser, JSONMessagePart, NetworkItem
|
from NetUtils import decode, encode, JSONtoTextParser, JSONMessagePart, NetworkItem, NetworkPlayer
|
||||||
from MultiServer import Endpoint
|
from MultiServer import Endpoint
|
||||||
from CommonClient import CommonContext, gui_enabled, ClientCommandProcessor, logger, get_base_parser
|
from CommonClient import CommonContext, gui_enabled, ClientCommandProcessor, logger, get_base_parser
|
||||||
|
|
||||||
@@ -101,12 +101,35 @@ class AHITContext(CommonContext):
|
|||||||
|
|
||||||
def on_package(self, cmd: str, args: dict):
|
def on_package(self, cmd: str, args: dict):
|
||||||
if cmd == "Connected":
|
if cmd == "Connected":
|
||||||
self.connected_msg = encode([args])
|
json = args
|
||||||
|
# This data is not needed and causes the game to freeze for long periods of time in large asyncs.
|
||||||
|
if "slot_info" in json.keys():
|
||||||
|
json["slot_info"] = {}
|
||||||
|
if "players" in json.keys():
|
||||||
|
me: NetworkPlayer
|
||||||
|
for n in json["players"]:
|
||||||
|
if n.slot == json["slot"] and n.team == json["team"]:
|
||||||
|
me = n
|
||||||
|
break
|
||||||
|
|
||||||
|
# Only put our player info in there as we actually need it
|
||||||
|
json["players"] = [me]
|
||||||
|
if DEBUG:
|
||||||
|
print(json)
|
||||||
|
self.connected_msg = encode([json])
|
||||||
if self.awaiting_info:
|
if self.awaiting_info:
|
||||||
self.server_msgs.append(self.room_info)
|
self.server_msgs.append(self.room_info)
|
||||||
self.update_items()
|
self.update_items()
|
||||||
self.awaiting_info = False
|
self.awaiting_info = False
|
||||||
|
|
||||||
|
elif cmd == "RoomUpdate":
|
||||||
|
# Same story as above
|
||||||
|
json = args
|
||||||
|
if "players" in json.keys():
|
||||||
|
json["players"] = []
|
||||||
|
|
||||||
|
self.server_msgs.append(encode(json))
|
||||||
|
|
||||||
elif cmd == "ReceivedItems":
|
elif cmd == "ReceivedItems":
|
||||||
if args["index"] == 0:
|
if args["index"] == 0:
|
||||||
self.full_inventory.clear()
|
self.full_inventory.clear()
|
||||||
@@ -166,6 +189,17 @@ async def proxy(websocket, path: str = "/", ctx: AHITContext = None):
|
|||||||
await ctx.disconnect_proxy()
|
await ctx.disconnect_proxy()
|
||||||
break
|
break
|
||||||
|
|
||||||
|
if ctx.auth:
|
||||||
|
name = msg.get("name", "")
|
||||||
|
if name != "" and name != ctx.auth:
|
||||||
|
logger.info("Aborting proxy connection: player name mismatch from save file")
|
||||||
|
logger.info(f"Expected: {ctx.auth}, got: {name}")
|
||||||
|
text = encode([{"cmd": "PrintJSON",
|
||||||
|
"data": [{"text": "Connection aborted - player name mismatch"}]}])
|
||||||
|
await ctx.send_msgs_proxy(text)
|
||||||
|
await ctx.disconnect_proxy()
|
||||||
|
break
|
||||||
|
|
||||||
if ctx.connected_msg and ctx.is_connected():
|
if ctx.connected_msg and ctx.is_connected():
|
||||||
await ctx.send_msgs_proxy(ctx.connected_msg)
|
await ctx.send_msgs_proxy(ctx.connected_msg)
|
||||||
ctx.update_items()
|
ctx.update_items()
|
||||||
|
|||||||
@@ -152,10 +152,10 @@ def create_dw_regions(world: "HatInTimeWorld"):
|
|||||||
for name in annoying_dws:
|
for name in annoying_dws:
|
||||||
world.excluded_dws.append(name)
|
world.excluded_dws.append(name)
|
||||||
|
|
||||||
if not world.options.DWEnableBonus or world.options.DWAutoCompleteBonuses:
|
if not world.options.DWEnableBonus and world.options.DWAutoCompleteBonuses:
|
||||||
for name in death_wishes:
|
for name in death_wishes:
|
||||||
world.excluded_bonuses.append(name)
|
world.excluded_bonuses.append(name)
|
||||||
elif world.options.DWExcludeAnnoyingBonuses:
|
if world.options.DWExcludeAnnoyingBonuses and not world.options.DWAutoCompleteBonuses:
|
||||||
for name in annoying_bonuses:
|
for name in annoying_bonuses:
|
||||||
world.excluded_bonuses.append(name)
|
world.excluded_bonuses.append(name)
|
||||||
|
|
||||||
|
|||||||
@@ -253,7 +253,8 @@ class HatInTimeWorld(World):
|
|||||||
else:
|
else:
|
||||||
item_name = loc.item.name
|
item_name = loc.item.name
|
||||||
|
|
||||||
shop_item_names.setdefault(str(loc.address), item_name)
|
shop_item_names.setdefault(str(loc.address),
|
||||||
|
f"{item_name} ({self.multiworld.get_player_name(loc.item.player)})")
|
||||||
|
|
||||||
slot_data["ShopItemNames"] = shop_item_names
|
slot_data["ShopItemNames"] = shop_item_names
|
||||||
|
|
||||||
|
|||||||
@@ -728,7 +728,7 @@ class ALttPPlandoConnections(PlandoConnections):
|
|||||||
entrances = set([connection[0] for connection in (
|
entrances = set([connection[0] for connection in (
|
||||||
*default_connections, *default_dungeon_connections, *inverted_default_connections,
|
*default_connections, *default_dungeon_connections, *inverted_default_connections,
|
||||||
*inverted_default_dungeon_connections)])
|
*inverted_default_dungeon_connections)])
|
||||||
exits = set([connection[1] for connection in (
|
exits = set([connection[0] for connection in (
|
||||||
*default_connections, *default_dungeon_connections, *inverted_default_connections,
|
*default_connections, *default_dungeon_connections, *inverted_default_connections,
|
||||||
*inverted_default_dungeon_connections)])
|
*inverted_default_dungeon_connections)])
|
||||||
|
|
||||||
|
|||||||
@@ -199,8 +199,6 @@ class BlasphemousWorld(World):
|
|||||||
|
|
||||||
self.multiworld.itempool += pool
|
self.multiworld.itempool += pool
|
||||||
|
|
||||||
|
|
||||||
def pre_fill(self):
|
|
||||||
self.place_items_from_dict(unrandomized_dict)
|
self.place_items_from_dict(unrandomized_dict)
|
||||||
|
|
||||||
if self.options.thorn_shuffle == "vanilla":
|
if self.options.thorn_shuffle == "vanilla":
|
||||||
@@ -335,4 +333,4 @@ class BlasphemousItem(Item):
|
|||||||
|
|
||||||
|
|
||||||
class BlasphemousLocation(Location):
|
class BlasphemousLocation(Location):
|
||||||
game: str = "Blasphemous"
|
game: str = "Blasphemous"
|
||||||
|
|||||||
@@ -125,6 +125,6 @@ class BumpStikWorld(World):
|
|||||||
lambda state: state.has("Hazard Bumper", self.player, 25)
|
lambda state: state.has("Hazard Bumper", self.player, 25)
|
||||||
|
|
||||||
self.multiworld.completion_condition[self.player] = \
|
self.multiworld.completion_condition[self.player] = \
|
||||||
lambda state: state.has("Booster Bumper", self.player, 5) and \
|
lambda state: state.has_all_counts({"Booster Bumper": 5, "Treasure Bumper": 32, "Hazard Bumper": 25}, \
|
||||||
state.has("Treasure Bumper", self.player, 32)
|
self.player)
|
||||||
|
|
||||||
|
|||||||
@@ -63,6 +63,9 @@ all_bosses = [
|
|||||||
DS3BossInfo("Deacons of the Deep", 3500800, locations = {
|
DS3BossInfo("Deacons of the Deep", 3500800, locations = {
|
||||||
"CD: Soul of the Deacons of the Deep",
|
"CD: Soul of the Deacons of the Deep",
|
||||||
"CD: Small Doll - boss drop",
|
"CD: Small Doll - boss drop",
|
||||||
|
"CD: Archdeacon White Crown - boss room after killing boss",
|
||||||
|
"CD: Archdeacon Holy Garb - boss room after killing boss",
|
||||||
|
"CD: Archdeacon Skirt - boss room after killing boss",
|
||||||
"FS: Hawkwood's Shield - gravestone after Hawkwood leaves",
|
"FS: Hawkwood's Shield - gravestone after Hawkwood leaves",
|
||||||
}),
|
}),
|
||||||
DS3BossInfo("Abyss Watchers", 3300801, before_storm_ruler = True, locations = {
|
DS3BossInfo("Abyss Watchers", 3300801, before_storm_ruler = True, locations = {
|
||||||
|
|||||||
@@ -612,9 +612,7 @@ class DarkSouls3World(World):
|
|||||||
self._add_entrance_rule("Painted World of Ariandel (Before Contraption)", "Basin of Vows")
|
self._add_entrance_rule("Painted World of Ariandel (Before Contraption)", "Basin of Vows")
|
||||||
|
|
||||||
# Define the access rules to some specific locations
|
# Define the access rules to some specific locations
|
||||||
if self._is_location_available("FS: Lift Chamber Key - Leonhard"):
|
self._add_location_rule("HWL: Red Eye Orb - wall tower, miniboss", "Lift Chamber Key")
|
||||||
self._add_location_rule("HWL: Red Eye Orb - wall tower, miniboss",
|
|
||||||
"Lift Chamber Key")
|
|
||||||
self._add_location_rule("ID: Bellowing Dragoncrest Ring - drop from B1 towards pit",
|
self._add_location_rule("ID: Bellowing Dragoncrest Ring - drop from B1 towards pit",
|
||||||
"Jailbreaker's Key")
|
"Jailbreaker's Key")
|
||||||
self._add_location_rule("ID: Covetous Gold Serpent Ring - Siegward's cell", "Old Cell Key")
|
self._add_location_rule("ID: Covetous Gold Serpent Ring - Siegward's cell", "Old Cell Key")
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
## Required Software
|
## Required Software
|
||||||
|
|
||||||
- [Dark Souls III](https://store.steampowered.com/app/374320/DARK_SOULS_III/)
|
- [Dark Souls III](https://store.steampowered.com/app/374320/DARK_SOULS_III/)
|
||||||
- [Dark Souls III AP Client](https://github.com/Marechal-L/Dark-Souls-III-Archipelago-client/releases)
|
- [Dark Souls III AP Client](https://github.com/nex3/Dark-Souls-III-Archipelago-client/releases/latest)
|
||||||
|
|
||||||
## Optional Software
|
## Optional Software
|
||||||
|
|
||||||
@@ -11,8 +11,9 @@
|
|||||||
|
|
||||||
## Setting Up
|
## Setting Up
|
||||||
|
|
||||||
First, download the client from the link above. It doesn't need to go into any particular directory;
|
First, download the client from the link above (`DS3.Archipelago.*.zip`). It doesn't need to go
|
||||||
it'll automatically locate _Dark Souls III_ in your Steam installation folder.
|
into any particular directory; it'll automatically locate _Dark Souls III_ in your Steam
|
||||||
|
installation folder.
|
||||||
|
|
||||||
Version 3.0.0 of the randomizer _only_ supports the latest version of _Dark Souls III_, 1.15.2. This
|
Version 3.0.0 of the randomizer _only_ supports the latest version of _Dark Souls III_, 1.15.2. This
|
||||||
is the latest version, so you don't need to do any downpatching! However, if you've already
|
is the latest version, so you don't need to do any downpatching! However, if you've already
|
||||||
@@ -35,8 +36,9 @@ randomized item and (optionally) enemy locations. You only need to do this once
|
|||||||
|
|
||||||
To run _Dark Souls III_ in Archipelago mode:
|
To run _Dark Souls III_ in Archipelago mode:
|
||||||
|
|
||||||
1. Start Steam. **Do not run in offline mode.** The mod will make sure you don't connect to the
|
1. Start Steam. **Do not run in offline mode.** Running Steam in offline mode will make certain
|
||||||
DS3 servers, and running Steam in offline mode will make certain scripted invaders fail to spawn.
|
scripted invaders fail to spawn. Instead, change the game itself to offline mode on the menu
|
||||||
|
screen.
|
||||||
|
|
||||||
2. Run `launchmod_darksouls3.bat`. This will start _Dark Souls III_ as well as a command prompt that
|
2. Run `launchmod_darksouls3.bat`. This will start _Dark Souls III_ as well as a command prompt that
|
||||||
you can use to interact with the Archipelago server.
|
you can use to interact with the Archipelago server.
|
||||||
@@ -52,4 +54,21 @@ To run _Dark Souls III_ in Archipelago mode:
|
|||||||
### Where do I get a config file?
|
### Where do I get a config file?
|
||||||
|
|
||||||
The [Player Options](/games/Dark%20Souls%20III/player-options) page on the website allows you to
|
The [Player Options](/games/Dark%20Souls%20III/player-options) page on the website allows you to
|
||||||
configure your personal options and export them into a config file.
|
configure your personal options and export them into a config file. The [AP client archive] also
|
||||||
|
includes an options template.
|
||||||
|
|
||||||
|
[AP client archive]: https://github.com/nex3/Dark-Souls-III-Archipelago-client/releases/latest
|
||||||
|
|
||||||
|
### Does this work with Proton?
|
||||||
|
|
||||||
|
The *Dark Souls III* Archipelago randomizer supports running on Linux under Proton. There are a few
|
||||||
|
things to keep in mind:
|
||||||
|
|
||||||
|
* Because `DS3Randomizer.exe` relies on the .NET runtime, you'll need to install
|
||||||
|
the [.NET Runtime] under **plain [WINE]**, then run `DS3Randomizer.exe` under
|
||||||
|
plain WINE as well. It won't work as a Proton app!
|
||||||
|
|
||||||
|
* To run the game itself, just run `launchmod_darksouls3.bat` under Proton.
|
||||||
|
|
||||||
|
[.NET Runtime]: https://dotnet.microsoft.com/en-us/download/dotnet/8.0
|
||||||
|
[WINE]: https://www.winehq.org/
|
||||||
|
|||||||
@@ -2214,13 +2214,13 @@ location_table: Dict[int, LocationDict] = {
|
|||||||
'map': 2,
|
'map': 2,
|
||||||
'index': 217,
|
'index': 217,
|
||||||
'doom_type': 2006,
|
'doom_type': 2006,
|
||||||
'region': "Perfect Hatred (E4M2) Blue"},
|
'region': "Perfect Hatred (E4M2) Upper"},
|
||||||
351367: {'name': 'Perfect Hatred (E4M2) - Exit',
|
351367: {'name': 'Perfect Hatred (E4M2) - Exit',
|
||||||
'episode': 4,
|
'episode': 4,
|
||||||
'map': 2,
|
'map': 2,
|
||||||
'index': -1,
|
'index': -1,
|
||||||
'doom_type': -1,
|
'doom_type': -1,
|
||||||
'region': "Perfect Hatred (E4M2) Blue"},
|
'region': "Perfect Hatred (E4M2) Upper"},
|
||||||
351368: {'name': 'Sever the Wicked (E4M3) - Invulnerability',
|
351368: {'name': 'Sever the Wicked (E4M3) - Invulnerability',
|
||||||
'episode': 4,
|
'episode': 4,
|
||||||
'map': 3,
|
'map': 3,
|
||||||
|
|||||||
@@ -502,13 +502,12 @@ regions:List[RegionDict] = [
|
|||||||
"episode":4,
|
"episode":4,
|
||||||
"connections":[
|
"connections":[
|
||||||
{"target":"Perfect Hatred (E4M2) Blue","pro":False},
|
{"target":"Perfect Hatred (E4M2) Blue","pro":False},
|
||||||
{"target":"Perfect Hatred (E4M2) Yellow","pro":False}]},
|
{"target":"Perfect Hatred (E4M2) Yellow","pro":False},
|
||||||
|
{"target":"Perfect Hatred (E4M2) Upper","pro":True}]},
|
||||||
{"name":"Perfect Hatred (E4M2) Blue",
|
{"name":"Perfect Hatred (E4M2) Blue",
|
||||||
"connects_to_hub":False,
|
"connects_to_hub":False,
|
||||||
"episode":4,
|
"episode":4,
|
||||||
"connections":[
|
"connections":[{"target":"Perfect Hatred (E4M2) Upper","pro":False}]},
|
||||||
{"target":"Perfect Hatred (E4M2) Main","pro":False},
|
|
||||||
{"target":"Perfect Hatred (E4M2) Cave","pro":False}]},
|
|
||||||
{"name":"Perfect Hatred (E4M2) Yellow",
|
{"name":"Perfect Hatred (E4M2) Yellow",
|
||||||
"connects_to_hub":False,
|
"connects_to_hub":False,
|
||||||
"episode":4,
|
"episode":4,
|
||||||
@@ -518,7 +517,13 @@ regions:List[RegionDict] = [
|
|||||||
{"name":"Perfect Hatred (E4M2) Cave",
|
{"name":"Perfect Hatred (E4M2) Cave",
|
||||||
"connects_to_hub":False,
|
"connects_to_hub":False,
|
||||||
"episode":4,
|
"episode":4,
|
||||||
"connections":[]},
|
"connections":[{"target":"Perfect Hatred (E4M2) Main","pro":False}]},
|
||||||
|
{"name":"Perfect Hatred (E4M2) Upper",
|
||||||
|
"connects_to_hub":False,
|
||||||
|
"episode":4,
|
||||||
|
"connections":[
|
||||||
|
{"target":"Perfect Hatred (E4M2) Cave","pro":False},
|
||||||
|
{"target":"Perfect Hatred (E4M2) Main","pro":False}]},
|
||||||
|
|
||||||
# Sever the Wicked (E4M3)
|
# Sever the Wicked (E4M3)
|
||||||
{"name":"Sever the Wicked (E4M3) Main",
|
{"name":"Sever the Wicked (E4M3) Main",
|
||||||
|
|||||||
@@ -403,9 +403,8 @@ def set_episode4_rules(player, multiworld, pro):
|
|||||||
state.has("Chaingun", player, 1)) and (state.has("Plasma gun", player, 1) or
|
state.has("Chaingun", player, 1)) and (state.has("Plasma gun", player, 1) or
|
||||||
state.has("BFG9000", player, 1)))
|
state.has("BFG9000", player, 1)))
|
||||||
set_rule(multiworld.get_entrance("Hell Beneath (E4M1) Main -> Hell Beneath (E4M1) Blue", player), lambda state:
|
set_rule(multiworld.get_entrance("Hell Beneath (E4M1) Main -> Hell Beneath (E4M1) Blue", player), lambda state:
|
||||||
state.has("Shotgun", player, 1) or
|
(state.has("Hell Beneath (E4M1) - Blue skull key", player, 1)) and (state.has("Shotgun", player, 1) or
|
||||||
state.has("Chaingun", player, 1) or
|
state.has("Chaingun", player, 1)))
|
||||||
state.has("Hell Beneath (E4M1) - Blue skull key", player, 1))
|
|
||||||
|
|
||||||
# Perfect Hatred (E4M2)
|
# Perfect Hatred (E4M2)
|
||||||
set_rule(multiworld.get_entrance("Hub -> Perfect Hatred (E4M2) Main", player), lambda state:
|
set_rule(multiworld.get_entrance("Hub -> Perfect Hatred (E4M2) Main", player), lambda state:
|
||||||
|
|||||||
@@ -1470,7 +1470,7 @@ location_table: Dict[int, LocationDict] = {
|
|||||||
'map': 6,
|
'map': 6,
|
||||||
'index': 102,
|
'index': 102,
|
||||||
'doom_type': 2006,
|
'doom_type': 2006,
|
||||||
'region': "Tenements (MAP17) Main"},
|
'region': "Tenements (MAP17) Yellow"},
|
||||||
361243: {'name': 'Tenements (MAP17) - Plasma gun',
|
361243: {'name': 'Tenements (MAP17) - Plasma gun',
|
||||||
'episode': 2,
|
'episode': 2,
|
||||||
'map': 6,
|
'map': 6,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
"""Outputs a Factorio Mod to facilitate integration with Archipelago"""
|
"""Outputs a Factorio Mod to facilitate integration with Archipelago"""
|
||||||
|
|
||||||
|
import dataclasses
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
@@ -88,6 +89,8 @@ class FactorioModFile(worlds.Files.APContainer):
|
|||||||
def generate_mod(world: "Factorio", output_directory: str):
|
def generate_mod(world: "Factorio", output_directory: str):
|
||||||
player = world.player
|
player = world.player
|
||||||
multiworld = world.multiworld
|
multiworld = world.multiworld
|
||||||
|
random = world.random
|
||||||
|
|
||||||
global data_final_template, locale_template, control_template, data_template, settings_template
|
global data_final_template, locale_template, control_template, data_template, settings_template
|
||||||
with template_load_lock:
|
with template_load_lock:
|
||||||
if not data_final_template:
|
if not data_final_template:
|
||||||
@@ -110,8 +113,6 @@ def generate_mod(world: "Factorio", output_directory: str):
|
|||||||
mod_name = f"AP-{multiworld.seed_name}-P{player}-{multiworld.get_file_safe_player_name(player)}"
|
mod_name = f"AP-{multiworld.seed_name}-P{player}-{multiworld.get_file_safe_player_name(player)}"
|
||||||
versioned_mod_name = mod_name + "_" + Utils.__version__
|
versioned_mod_name = mod_name + "_" + Utils.__version__
|
||||||
|
|
||||||
random = multiworld.per_slot_randoms[player]
|
|
||||||
|
|
||||||
def flop_random(low, high, base=None):
|
def flop_random(low, high, base=None):
|
||||||
"""Guarantees 50% below base and 50% above base, uniform distribution in each direction."""
|
"""Guarantees 50% below base and 50% above base, uniform distribution in each direction."""
|
||||||
if base:
|
if base:
|
||||||
@@ -129,43 +130,43 @@ def generate_mod(world: "Factorio", output_directory: str):
|
|||||||
"base_tech_table": base_tech_table,
|
"base_tech_table": base_tech_table,
|
||||||
"tech_to_progressive_lookup": tech_to_progressive_lookup,
|
"tech_to_progressive_lookup": tech_to_progressive_lookup,
|
||||||
"mod_name": mod_name,
|
"mod_name": mod_name,
|
||||||
"allowed_science_packs": multiworld.max_science_pack[player].get_allowed_packs(),
|
"allowed_science_packs": world.options.max_science_pack.get_allowed_packs(),
|
||||||
"custom_technologies": multiworld.worlds[player].custom_technologies,
|
"custom_technologies": world.custom_technologies,
|
||||||
"tech_tree_layout_prerequisites": world.tech_tree_layout_prerequisites,
|
"tech_tree_layout_prerequisites": world.tech_tree_layout_prerequisites,
|
||||||
"slot_name": multiworld.player_name[player], "seed_name": multiworld.seed_name,
|
"slot_name": world.player_name, "seed_name": multiworld.seed_name,
|
||||||
"slot_player": player,
|
"slot_player": player,
|
||||||
"starting_items": multiworld.starting_items[player], "recipes": recipes,
|
"starting_items": world.options.starting_items, "recipes": recipes,
|
||||||
"random": random, "flop_random": flop_random,
|
"random": random, "flop_random": flop_random,
|
||||||
"recipe_time_scale": recipe_time_scales.get(multiworld.recipe_time[player].value, None),
|
"recipe_time_scale": recipe_time_scales.get(world.options.recipe_time.value, None),
|
||||||
"recipe_time_range": recipe_time_ranges.get(multiworld.recipe_time[player].value, None),
|
"recipe_time_range": recipe_time_ranges.get(world.options.recipe_time.value, None),
|
||||||
"free_sample_blacklist": {item: 1 for item in free_sample_exclusions},
|
"free_sample_blacklist": {item: 1 for item in free_sample_exclusions},
|
||||||
"progressive_technology_table": {tech.name: tech.progressive for tech in
|
"progressive_technology_table": {tech.name: tech.progressive for tech in
|
||||||
progressive_technology_table.values()},
|
progressive_technology_table.values()},
|
||||||
"custom_recipes": world.custom_recipes,
|
"custom_recipes": world.custom_recipes,
|
||||||
"max_science_pack": multiworld.max_science_pack[player].value,
|
"max_science_pack": world.options.max_science_pack.value,
|
||||||
"liquids": fluids,
|
"liquids": fluids,
|
||||||
"goal": multiworld.goal[player].value,
|
"goal": world.options.goal.value,
|
||||||
"energy_link": multiworld.energy_link[player].value,
|
"energy_link": world.options.energy_link.value,
|
||||||
"useless_technologies": useless_technologies,
|
"useless_technologies": useless_technologies,
|
||||||
"chunk_shuffle": multiworld.chunk_shuffle[player].value if hasattr(multiworld, "chunk_shuffle") else 0,
|
"chunk_shuffle": 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
for factorio_option in Options.factorio_options:
|
for factorio_option, factorio_option_instance in dataclasses.asdict(world.options).items():
|
||||||
if factorio_option in ["free_sample_blacklist", "free_sample_whitelist"]:
|
if factorio_option in ["free_sample_blacklist", "free_sample_whitelist"]:
|
||||||
continue
|
continue
|
||||||
template_data[factorio_option] = getattr(multiworld, factorio_option)[player].value
|
template_data[factorio_option] = factorio_option_instance.value
|
||||||
|
|
||||||
if getattr(multiworld, "silo")[player].value == Options.Silo.option_randomize_recipe:
|
if world.options.silo == Options.Silo.option_randomize_recipe:
|
||||||
template_data["free_sample_blacklist"]["rocket-silo"] = 1
|
template_data["free_sample_blacklist"]["rocket-silo"] = 1
|
||||||
|
|
||||||
if getattr(multiworld, "satellite")[player].value == Options.Satellite.option_randomize_recipe:
|
if world.options.satellite == Options.Satellite.option_randomize_recipe:
|
||||||
template_data["free_sample_blacklist"]["satellite"] = 1
|
template_data["free_sample_blacklist"]["satellite"] = 1
|
||||||
|
|
||||||
template_data["free_sample_blacklist"].update({item: 1 for item in multiworld.free_sample_blacklist[player].value})
|
template_data["free_sample_blacklist"].update({item: 1 for item in world.options.free_sample_blacklist.value})
|
||||||
template_data["free_sample_blacklist"].update({item: 0 for item in multiworld.free_sample_whitelist[player].value})
|
template_data["free_sample_blacklist"].update({item: 0 for item in world.options.free_sample_whitelist.value})
|
||||||
|
|
||||||
zf_path = os.path.join(output_directory, versioned_mod_name + ".zip")
|
zf_path = os.path.join(output_directory, versioned_mod_name + ".zip")
|
||||||
mod = FactorioModFile(zf_path, player=player, player_name=multiworld.player_name[player])
|
mod = FactorioModFile(zf_path, player=player, player_name=world.player_name)
|
||||||
|
|
||||||
if world.zip_path:
|
if world.zip_path:
|
||||||
with zipfile.ZipFile(world.zip_path) as zf:
|
with zipfile.ZipFile(world.zip_path) as zf:
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
import typing
|
|
||||||
|
from dataclasses import dataclass
|
||||||
import datetime
|
import datetime
|
||||||
|
import typing
|
||||||
|
|
||||||
|
from schema import Schema, Optional, And, Or
|
||||||
|
|
||||||
from Options import Choice, OptionDict, OptionSet, Option, DefaultOnToggle, Range, DeathLink, Toggle, \
|
from Options import Choice, OptionDict, OptionSet, Option, DefaultOnToggle, Range, DeathLink, Toggle, \
|
||||||
StartInventoryPool
|
StartInventoryPool, PerGameCommonOptions
|
||||||
from schema import Schema, Optional, And, Or
|
|
||||||
|
|
||||||
# schema helpers
|
# schema helpers
|
||||||
FloatRange = lambda low, high: And(Or(int, float), lambda f: low <= f <= high)
|
FloatRange = lambda low, high: And(Or(int, float), lambda f: low <= f <= high)
|
||||||
@@ -422,50 +425,37 @@ class EnergyLink(Toggle):
|
|||||||
display_name = "EnergyLink"
|
display_name = "EnergyLink"
|
||||||
|
|
||||||
|
|
||||||
factorio_options: typing.Dict[str, type(Option)] = {
|
@dataclass
|
||||||
"max_science_pack": MaxSciencePack,
|
class FactorioOptions(PerGameCommonOptions):
|
||||||
"goal": Goal,
|
max_science_pack: MaxSciencePack
|
||||||
"tech_tree_layout": TechTreeLayout,
|
goal: Goal
|
||||||
"min_tech_cost": MinTechCost,
|
tech_tree_layout: TechTreeLayout
|
||||||
"max_tech_cost": MaxTechCost,
|
min_tech_cost: MinTechCost
|
||||||
"tech_cost_distribution": TechCostDistribution,
|
max_tech_cost: MaxTechCost
|
||||||
"tech_cost_mix": TechCostMix,
|
tech_cost_distribution: TechCostDistribution
|
||||||
"ramping_tech_costs": RampingTechCosts,
|
tech_cost_mix: TechCostMix
|
||||||
"silo": Silo,
|
ramping_tech_costs: RampingTechCosts
|
||||||
"satellite": Satellite,
|
silo: Silo
|
||||||
"free_samples": FreeSamples,
|
satellite: Satellite
|
||||||
"tech_tree_information": TechTreeInformation,
|
free_samples: FreeSamples
|
||||||
"starting_items": FactorioStartItems,
|
tech_tree_information: TechTreeInformation
|
||||||
"free_sample_blacklist": FactorioFreeSampleBlacklist,
|
starting_items: FactorioStartItems
|
||||||
"free_sample_whitelist": FactorioFreeSampleWhitelist,
|
free_sample_blacklist: FactorioFreeSampleBlacklist
|
||||||
"recipe_time": RecipeTime,
|
free_sample_whitelist: FactorioFreeSampleWhitelist
|
||||||
"recipe_ingredients": RecipeIngredients,
|
recipe_time: RecipeTime
|
||||||
"recipe_ingredients_offset": RecipeIngredientsOffset,
|
recipe_ingredients: RecipeIngredients
|
||||||
"imported_blueprints": ImportedBlueprint,
|
recipe_ingredients_offset: RecipeIngredientsOffset
|
||||||
"world_gen": FactorioWorldGen,
|
imported_blueprints: ImportedBlueprint
|
||||||
"progressive": Progressive,
|
world_gen: FactorioWorldGen
|
||||||
"teleport_traps": TeleportTrapCount,
|
progressive: Progressive
|
||||||
"grenade_traps": GrenadeTrapCount,
|
teleport_traps: TeleportTrapCount
|
||||||
"cluster_grenade_traps": ClusterGrenadeTrapCount,
|
grenade_traps: GrenadeTrapCount
|
||||||
"artillery_traps": ArtilleryTrapCount,
|
cluster_grenade_traps: ClusterGrenadeTrapCount
|
||||||
"atomic_rocket_traps": AtomicRocketTrapCount,
|
artillery_traps: ArtilleryTrapCount
|
||||||
"attack_traps": AttackTrapCount,
|
atomic_rocket_traps: AtomicRocketTrapCount
|
||||||
"evolution_traps": EvolutionTrapCount,
|
attack_traps: AttackTrapCount
|
||||||
"evolution_trap_increase": EvolutionTrapIncrease,
|
evolution_traps: EvolutionTrapCount
|
||||||
"death_link": DeathLink,
|
evolution_trap_increase: EvolutionTrapIncrease
|
||||||
"energy_link": EnergyLink,
|
death_link: DeathLink
|
||||||
"start_inventory_from_pool": StartInventoryPool,
|
energy_link: EnergyLink
|
||||||
}
|
start_inventory_from_pool: StartInventoryPool
|
||||||
|
|
||||||
# spoilers below. If you spoil it for yourself, please at least don't spoil it for anyone else.
|
|
||||||
if datetime.datetime.today().month == 4:
|
|
||||||
|
|
||||||
class ChunkShuffle(Toggle):
|
|
||||||
"""Entrance Randomizer."""
|
|
||||||
display_name = "Chunk Shuffle"
|
|
||||||
|
|
||||||
|
|
||||||
if datetime.datetime.today().day > 1:
|
|
||||||
ChunkShuffle.__doc__ += """
|
|
||||||
2023 April Fool's option. Shuffles chunk border transitions."""
|
|
||||||
factorio_options["chunk_shuffle"] = ChunkShuffle
|
|
||||||
|
|||||||
@@ -19,12 +19,10 @@ def _sorter(location: "FactorioScienceLocation"):
|
|||||||
return location.complexity, location.rel_cost
|
return location.complexity, location.rel_cost
|
||||||
|
|
||||||
|
|
||||||
def get_shapes(factorio_world: "Factorio") -> Dict["FactorioScienceLocation", Set["FactorioScienceLocation"]]:
|
def get_shapes(world: "Factorio") -> Dict["FactorioScienceLocation", Set["FactorioScienceLocation"]]:
|
||||||
world = factorio_world.multiworld
|
|
||||||
player = factorio_world.player
|
|
||||||
prerequisites: Dict["FactorioScienceLocation", Set["FactorioScienceLocation"]] = {}
|
prerequisites: Dict["FactorioScienceLocation", Set["FactorioScienceLocation"]] = {}
|
||||||
layout = world.tech_tree_layout[player].value
|
layout = world.options.tech_tree_layout.value
|
||||||
locations: List["FactorioScienceLocation"] = sorted(factorio_world.science_locations, key=lambda loc: loc.name)
|
locations: List["FactorioScienceLocation"] = sorted(world.science_locations, key=lambda loc: loc.name)
|
||||||
world.random.shuffle(locations)
|
world.random.shuffle(locations)
|
||||||
|
|
||||||
if layout == TechTreeLayout.option_single:
|
if layout == TechTreeLayout.option_single:
|
||||||
@@ -247,5 +245,5 @@ def get_shapes(factorio_world: "Factorio") -> Dict["FactorioScienceLocation", Se
|
|||||||
else:
|
else:
|
||||||
raise NotImplementedError(f"Layout {layout} is not implemented.")
|
raise NotImplementedError(f"Layout {layout} is not implemented.")
|
||||||
|
|
||||||
factorio_world.tech_tree_layout_prerequisites = prerequisites
|
world.tech_tree_layout_prerequisites = prerequisites
|
||||||
return prerequisites
|
return prerequisites
|
||||||
|
|||||||
@@ -13,12 +13,11 @@ import Utils
|
|||||||
from . import Options
|
from . import Options
|
||||||
|
|
||||||
factorio_tech_id = factorio_base_id = 2 ** 17
|
factorio_tech_id = factorio_base_id = 2 ** 17
|
||||||
# Factorio technologies are imported from a .json document in /data
|
|
||||||
source_folder = os.path.join(os.path.dirname(__file__), "data")
|
|
||||||
|
|
||||||
pool = ThreadPoolExecutor(1)
|
pool = ThreadPoolExecutor(1)
|
||||||
|
|
||||||
|
|
||||||
|
# Factorio technologies are imported from a .json document in /data
|
||||||
def load_json_data(data_name: str) -> Union[List[str], Dict[str, Any]]:
|
def load_json_data(data_name: str) -> Union[List[str], Dict[str, Any]]:
|
||||||
return orjson.loads(pkgutil.get_data(__name__, "data/" + data_name + ".json"))
|
return orjson.loads(pkgutil.get_data(__name__, "data/" + data_name + ".json"))
|
||||||
|
|
||||||
@@ -99,7 +98,7 @@ class CustomTechnology(Technology):
|
|||||||
and ((ingredients & {"chemical-science-pack", "production-science-pack", "utility-science-pack"})
|
and ((ingredients & {"chemical-science-pack", "production-science-pack", "utility-science-pack"})
|
||||||
or origin.name == "rocket-silo")
|
or origin.name == "rocket-silo")
|
||||||
self.player = player
|
self.player = player
|
||||||
if origin.name not in world.worlds[player].special_nodes:
|
if origin.name not in world.special_nodes:
|
||||||
if military_allowed:
|
if military_allowed:
|
||||||
ingredients.add("military-science-pack")
|
ingredients.add("military-science-pack")
|
||||||
ingredients = list(ingredients)
|
ingredients = list(ingredients)
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ from worlds.LauncherComponents import Component, components, Type, launch_subpro
|
|||||||
from worlds.generic import Rules
|
from worlds.generic import Rules
|
||||||
from .Locations import location_pools, location_table
|
from .Locations import location_pools, location_table
|
||||||
from .Mod import generate_mod
|
from .Mod import generate_mod
|
||||||
from .Options import factorio_options, MaxSciencePack, Silo, Satellite, TechTreeInformation, Goal, TechCostDistribution
|
from .Options import FactorioOptions, MaxSciencePack, Silo, Satellite, TechTreeInformation, Goal, TechCostDistribution
|
||||||
from .Shapes import get_shapes
|
from .Shapes import get_shapes
|
||||||
from .Technologies import base_tech_table, recipe_sources, base_technology_table, \
|
from .Technologies import base_tech_table, recipe_sources, base_technology_table, \
|
||||||
all_ingredient_names, all_product_sources, required_technologies, get_rocket_requirements, \
|
all_ingredient_names, all_product_sources, required_technologies, get_rocket_requirements, \
|
||||||
@@ -89,13 +89,15 @@ class Factorio(World):
|
|||||||
advancement_technologies: typing.Set[str]
|
advancement_technologies: typing.Set[str]
|
||||||
|
|
||||||
web = FactorioWeb()
|
web = FactorioWeb()
|
||||||
|
options_dataclass = FactorioOptions
|
||||||
|
options: FactorioOptions
|
||||||
|
|
||||||
item_name_to_id = all_items
|
item_name_to_id = all_items
|
||||||
location_name_to_id = location_table
|
location_name_to_id = location_table
|
||||||
item_name_groups = {
|
item_name_groups = {
|
||||||
"Progressive": set(progressive_tech_table.keys()),
|
"Progressive": set(progressive_tech_table.keys()),
|
||||||
}
|
}
|
||||||
required_client_version = (0, 4, 2)
|
required_client_version = (0, 5, 0)
|
||||||
|
|
||||||
ordered_science_packs: typing.List[str] = MaxSciencePack.get_ordered_science_packs()
|
ordered_science_packs: typing.List[str] = MaxSciencePack.get_ordered_science_packs()
|
||||||
tech_tree_layout_prerequisites: typing.Dict[FactorioScienceLocation, typing.Set[FactorioScienceLocation]]
|
tech_tree_layout_prerequisites: typing.Dict[FactorioScienceLocation, typing.Set[FactorioScienceLocation]]
|
||||||
@@ -117,32 +119,32 @@ class Factorio(World):
|
|||||||
|
|
||||||
def generate_early(self) -> None:
|
def generate_early(self) -> None:
|
||||||
# if max < min, then swap max and min
|
# if max < min, then swap max and min
|
||||||
if self.multiworld.max_tech_cost[self.player] < self.multiworld.min_tech_cost[self.player]:
|
if self.options.max_tech_cost < self.options.min_tech_cost:
|
||||||
self.multiworld.min_tech_cost[self.player].value, self.multiworld.max_tech_cost[self.player].value = \
|
self.options.min_tech_cost.value, self.options.max_tech_cost.value = \
|
||||||
self.multiworld.max_tech_cost[self.player].value, self.multiworld.min_tech_cost[self.player].value
|
self.options.max_tech_cost.value, self.options.min_tech_cost.value
|
||||||
self.tech_mix = self.multiworld.tech_cost_mix[self.player]
|
self.tech_mix = self.options.tech_cost_mix.value
|
||||||
self.skip_silo = self.multiworld.silo[self.player].value == Silo.option_spawn
|
self.skip_silo = self.options.silo.value == Silo.option_spawn
|
||||||
|
|
||||||
def create_regions(self):
|
def create_regions(self):
|
||||||
player = self.player
|
player = self.player
|
||||||
random = self.multiworld.random
|
random = self.random
|
||||||
nauvis = Region("Nauvis", player, self.multiworld)
|
nauvis = Region("Nauvis", player, self.multiworld)
|
||||||
|
|
||||||
location_count = len(base_tech_table) - len(useless_technologies) - self.skip_silo + \
|
location_count = len(base_tech_table) - len(useless_technologies) - self.skip_silo + \
|
||||||
self.multiworld.evolution_traps[player] + \
|
self.options.evolution_traps + \
|
||||||
self.multiworld.attack_traps[player] + \
|
self.options.attack_traps + \
|
||||||
self.multiworld.teleport_traps[player] + \
|
self.options.teleport_traps + \
|
||||||
self.multiworld.grenade_traps[player] + \
|
self.options.grenade_traps + \
|
||||||
self.multiworld.cluster_grenade_traps[player] + \
|
self.options.cluster_grenade_traps + \
|
||||||
self.multiworld.atomic_rocket_traps[player] + \
|
self.options.atomic_rocket_traps + \
|
||||||
self.multiworld.artillery_traps[player]
|
self.options.artillery_traps
|
||||||
|
|
||||||
location_pool = []
|
location_pool = []
|
||||||
|
|
||||||
for pack in sorted(self.multiworld.max_science_pack[self.player].get_allowed_packs()):
|
for pack in sorted(self.options.max_science_pack.get_allowed_packs()):
|
||||||
location_pool.extend(location_pools[pack])
|
location_pool.extend(location_pools[pack])
|
||||||
try:
|
try:
|
||||||
location_names = self.multiworld.random.sample(location_pool, location_count)
|
location_names = random.sample(location_pool, location_count)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
# should be "ValueError: Sample larger than population or is negative"
|
# should be "ValueError: Sample larger than population or is negative"
|
||||||
raise Exception("Too many traps for too few locations. Either decrease the trap count, "
|
raise Exception("Too many traps for too few locations. Either decrease the trap count, "
|
||||||
@@ -150,9 +152,9 @@ class Factorio(World):
|
|||||||
|
|
||||||
self.science_locations = [FactorioScienceLocation(player, loc_name, self.location_name_to_id[loc_name], nauvis)
|
self.science_locations = [FactorioScienceLocation(player, loc_name, self.location_name_to_id[loc_name], nauvis)
|
||||||
for loc_name in location_names]
|
for loc_name in location_names]
|
||||||
distribution: TechCostDistribution = self.multiworld.tech_cost_distribution[self.player]
|
distribution: TechCostDistribution = self.options.tech_cost_distribution
|
||||||
min_cost = self.multiworld.min_tech_cost[self.player]
|
min_cost = self.options.min_tech_cost.value
|
||||||
max_cost = self.multiworld.max_tech_cost[self.player]
|
max_cost = self.options.max_tech_cost.value
|
||||||
if distribution == distribution.option_even:
|
if distribution == distribution.option_even:
|
||||||
rand_values = (random.randint(min_cost, max_cost) for _ in self.science_locations)
|
rand_values = (random.randint(min_cost, max_cost) for _ in self.science_locations)
|
||||||
else:
|
else:
|
||||||
@@ -161,7 +163,7 @@ class Factorio(World):
|
|||||||
distribution.option_high: max_cost}[distribution.value]
|
distribution.option_high: max_cost}[distribution.value]
|
||||||
rand_values = (random.triangular(min_cost, max_cost, mode) for _ in self.science_locations)
|
rand_values = (random.triangular(min_cost, max_cost, mode) for _ in self.science_locations)
|
||||||
rand_values = sorted(rand_values)
|
rand_values = sorted(rand_values)
|
||||||
if self.multiworld.ramping_tech_costs[self.player]:
|
if self.options.ramping_tech_costs:
|
||||||
def sorter(loc: FactorioScienceLocation):
|
def sorter(loc: FactorioScienceLocation):
|
||||||
return loc.complexity, loc.rel_cost
|
return loc.complexity, loc.rel_cost
|
||||||
else:
|
else:
|
||||||
@@ -176,7 +178,7 @@ class Factorio(World):
|
|||||||
event = FactorioItem("Victory", ItemClassification.progression, None, player)
|
event = FactorioItem("Victory", ItemClassification.progression, None, player)
|
||||||
location.place_locked_item(event)
|
location.place_locked_item(event)
|
||||||
|
|
||||||
for ingredient in sorted(self.multiworld.max_science_pack[self.player].get_allowed_packs()):
|
for ingredient in sorted(self.options.max_science_pack.get_allowed_packs()):
|
||||||
location = FactorioLocation(player, f"Automate {ingredient}", None, nauvis)
|
location = FactorioLocation(player, f"Automate {ingredient}", None, nauvis)
|
||||||
nauvis.locations.append(location)
|
nauvis.locations.append(location)
|
||||||
event = FactorioItem(f"Automated {ingredient}", ItemClassification.progression, None, player)
|
event = FactorioItem(f"Automated {ingredient}", ItemClassification.progression, None, player)
|
||||||
@@ -185,24 +187,23 @@ class Factorio(World):
|
|||||||
self.multiworld.regions.append(nauvis)
|
self.multiworld.regions.append(nauvis)
|
||||||
|
|
||||||
def create_items(self) -> None:
|
def create_items(self) -> None:
|
||||||
player = self.player
|
|
||||||
self.custom_technologies = self.set_custom_technologies()
|
self.custom_technologies = self.set_custom_technologies()
|
||||||
self.set_custom_recipes()
|
self.set_custom_recipes()
|
||||||
traps = ("Evolution", "Attack", "Teleport", "Grenade", "Cluster Grenade", "Artillery", "Atomic Rocket")
|
traps = ("Evolution", "Attack", "Teleport", "Grenade", "Cluster Grenade", "Artillery", "Atomic Rocket")
|
||||||
for trap_name in traps:
|
for trap_name in traps:
|
||||||
self.multiworld.itempool.extend(self.create_item(f"{trap_name} Trap") for _ in
|
self.multiworld.itempool.extend(self.create_item(f"{trap_name} Trap") for _ in
|
||||||
range(getattr(self.multiworld,
|
range(getattr(self.options,
|
||||||
f"{trap_name.lower().replace(' ', '_')}_traps")[player]))
|
f"{trap_name.lower().replace(' ', '_')}_traps")))
|
||||||
|
|
||||||
want_progressives = collections.defaultdict(lambda: self.multiworld.progressive[player].
|
want_progressives = collections.defaultdict(lambda: self.options.progressive.
|
||||||
want_progressives(self.multiworld.random))
|
want_progressives(self.random))
|
||||||
|
|
||||||
cost_sorted_locations = sorted(self.science_locations, key=lambda location: location.name)
|
cost_sorted_locations = sorted(self.science_locations, key=lambda location: location.name)
|
||||||
special_index = {"automation": 0,
|
special_index = {"automation": 0,
|
||||||
"logistics": 1,
|
"logistics": 1,
|
||||||
"rocket-silo": -1}
|
"rocket-silo": -1}
|
||||||
loc: FactorioScienceLocation
|
loc: FactorioScienceLocation
|
||||||
if self.multiworld.tech_tree_information[player] == TechTreeInformation.option_full:
|
if self.options.tech_tree_information == TechTreeInformation.option_full:
|
||||||
# mark all locations as pre-hinted
|
# mark all locations as pre-hinted
|
||||||
for loc in self.science_locations:
|
for loc in self.science_locations:
|
||||||
loc.revealed = True
|
loc.revealed = True
|
||||||
@@ -229,14 +230,13 @@ class Factorio(World):
|
|||||||
loc.revealed = True
|
loc.revealed = True
|
||||||
|
|
||||||
def set_rules(self):
|
def set_rules(self):
|
||||||
world = self.multiworld
|
|
||||||
player = self.player
|
player = self.player
|
||||||
shapes = get_shapes(self)
|
shapes = get_shapes(self)
|
||||||
|
|
||||||
for ingredient in self.multiworld.max_science_pack[self.player].get_allowed_packs():
|
for ingredient in self.options.max_science_pack.get_allowed_packs():
|
||||||
location = world.get_location(f"Automate {ingredient}", player)
|
location = self.get_location(f"Automate {ingredient}")
|
||||||
|
|
||||||
if self.multiworld.recipe_ingredients[self.player]:
|
if self.options.recipe_ingredients:
|
||||||
custom_recipe = self.custom_recipes[ingredient]
|
custom_recipe = self.custom_recipes[ingredient]
|
||||||
|
|
||||||
location.access_rule = lambda state, ingredient=ingredient, custom_recipe=custom_recipe: \
|
location.access_rule = lambda state, ingredient=ingredient, custom_recipe=custom_recipe: \
|
||||||
@@ -257,30 +257,30 @@ class Factorio(World):
|
|||||||
prerequisites: all(state.can_reach(loc) for loc in locations))
|
prerequisites: all(state.can_reach(loc) for loc in locations))
|
||||||
|
|
||||||
silo_recipe = None
|
silo_recipe = None
|
||||||
if self.multiworld.silo[self.player] == Silo.option_spawn:
|
if self.options.silo == Silo.option_spawn:
|
||||||
silo_recipe = self.custom_recipes["rocket-silo"] if "rocket-silo" in self.custom_recipes \
|
silo_recipe = self.custom_recipes["rocket-silo"] if "rocket-silo" in self.custom_recipes \
|
||||||
else next(iter(all_product_sources.get("rocket-silo")))
|
else next(iter(all_product_sources.get("rocket-silo")))
|
||||||
part_recipe = self.custom_recipes["rocket-part"]
|
part_recipe = self.custom_recipes["rocket-part"]
|
||||||
satellite_recipe = None
|
satellite_recipe = None
|
||||||
if self.multiworld.goal[self.player] == Goal.option_satellite:
|
if self.options.goal == Goal.option_satellite:
|
||||||
satellite_recipe = self.custom_recipes["satellite"] if "satellite" in self.custom_recipes \
|
satellite_recipe = self.custom_recipes["satellite"] if "satellite" in self.custom_recipes \
|
||||||
else next(iter(all_product_sources.get("satellite")))
|
else next(iter(all_product_sources.get("satellite")))
|
||||||
victory_tech_names = get_rocket_requirements(silo_recipe, part_recipe, satellite_recipe)
|
victory_tech_names = get_rocket_requirements(silo_recipe, part_recipe, satellite_recipe)
|
||||||
if self.multiworld.silo[self.player] != Silo.option_spawn:
|
if self.options.silo != Silo.option_spawn:
|
||||||
victory_tech_names.add("rocket-silo")
|
victory_tech_names.add("rocket-silo")
|
||||||
world.get_location("Rocket Launch", player).access_rule = lambda state: all(state.has(technology, player)
|
self.get_location("Rocket Launch").access_rule = lambda state: all(state.has(technology, player)
|
||||||
for technology in
|
for technology in
|
||||||
victory_tech_names)
|
victory_tech_names)
|
||||||
|
|
||||||
world.completion_condition[player] = lambda state: state.has('Victory', player)
|
self.multiworld.completion_condition[player] = lambda state: state.has('Victory', player)
|
||||||
|
|
||||||
def generate_basic(self):
|
def generate_basic(self):
|
||||||
map_basic_settings = self.multiworld.world_gen[self.player].value["basic"]
|
map_basic_settings = self.options.world_gen.value["basic"]
|
||||||
if map_basic_settings.get("seed", None) is None: # allow seed 0
|
if map_basic_settings.get("seed", None) is None: # allow seed 0
|
||||||
# 32 bit uint
|
# 32 bit uint
|
||||||
map_basic_settings["seed"] = self.multiworld.per_slot_randoms[self.player].randint(0, 2 ** 32 - 1)
|
map_basic_settings["seed"] = self.random.randint(0, 2 ** 32 - 1)
|
||||||
|
|
||||||
start_location_hints: typing.Set[str] = self.multiworld.start_location_hints[self.player].value
|
start_location_hints: typing.Set[str] = self.options.start_location_hints.value
|
||||||
|
|
||||||
for loc in self.science_locations:
|
for loc in self.science_locations:
|
||||||
# show start_location_hints ingame
|
# show start_location_hints ingame
|
||||||
@@ -304,8 +304,6 @@ class Factorio(World):
|
|||||||
|
|
||||||
return super(Factorio, self).collect_item(state, item, remove)
|
return super(Factorio, self).collect_item(state, item, remove)
|
||||||
|
|
||||||
option_definitions = factorio_options
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def stage_write_spoiler(cls, world, spoiler_handle):
|
def stage_write_spoiler(cls, world, spoiler_handle):
|
||||||
factorio_players = world.get_game_players(cls.game)
|
factorio_players = world.get_game_players(cls.game)
|
||||||
@@ -345,7 +343,7 @@ class Factorio(World):
|
|||||||
# have to first sort for determinism, while filtering out non-stacking items
|
# have to first sort for determinism, while filtering out non-stacking items
|
||||||
pool: typing.List[str] = sorted(pool & valid_ingredients)
|
pool: typing.List[str] = sorted(pool & valid_ingredients)
|
||||||
# then sort with random data to shuffle
|
# then sort with random data to shuffle
|
||||||
self.multiworld.random.shuffle(pool)
|
self.random.shuffle(pool)
|
||||||
target_raw = int(sum((count for ingredient, count in original.base_cost.items())) * factor)
|
target_raw = int(sum((count for ingredient, count in original.base_cost.items())) * factor)
|
||||||
target_energy = original.total_energy * factor
|
target_energy = original.total_energy * factor
|
||||||
target_num_ingredients = len(original.ingredients) + ingredients_offset
|
target_num_ingredients = len(original.ingredients) + ingredients_offset
|
||||||
@@ -389,7 +387,7 @@ class Factorio(World):
|
|||||||
if min_num > max_num:
|
if min_num > max_num:
|
||||||
fallback_pool.append(ingredient)
|
fallback_pool.append(ingredient)
|
||||||
continue # can't use that ingredient
|
continue # can't use that ingredient
|
||||||
num = self.multiworld.random.randint(min_num, max_num)
|
num = self.random.randint(min_num, max_num)
|
||||||
new_ingredients[ingredient] = num
|
new_ingredients[ingredient] = num
|
||||||
remaining_raw -= num * ingredient_raw
|
remaining_raw -= num * ingredient_raw
|
||||||
remaining_energy -= num * ingredient_energy
|
remaining_energy -= num * ingredient_energy
|
||||||
@@ -433,66 +431,66 @@ class Factorio(World):
|
|||||||
|
|
||||||
def set_custom_technologies(self):
|
def set_custom_technologies(self):
|
||||||
custom_technologies = {}
|
custom_technologies = {}
|
||||||
allowed_packs = self.multiworld.max_science_pack[self.player].get_allowed_packs()
|
allowed_packs = self.options.max_science_pack.get_allowed_packs()
|
||||||
for technology_name, technology in base_technology_table.items():
|
for technology_name, technology in base_technology_table.items():
|
||||||
custom_technologies[technology_name] = technology.get_custom(self.multiworld, allowed_packs, self.player)
|
custom_technologies[technology_name] = technology.get_custom(self, allowed_packs, self.player)
|
||||||
return custom_technologies
|
return custom_technologies
|
||||||
|
|
||||||
def set_custom_recipes(self):
|
def set_custom_recipes(self):
|
||||||
ingredients_offset = self.multiworld.recipe_ingredients_offset[self.player]
|
ingredients_offset = self.options.recipe_ingredients_offset
|
||||||
original_rocket_part = recipes["rocket-part"]
|
original_rocket_part = recipes["rocket-part"]
|
||||||
science_pack_pools = get_science_pack_pools()
|
science_pack_pools = get_science_pack_pools()
|
||||||
valid_pool = sorted(science_pack_pools[self.multiworld.max_science_pack[self.player].get_max_pack()] & valid_ingredients)
|
valid_pool = sorted(science_pack_pools[self.options.max_science_pack.get_max_pack()] & valid_ingredients)
|
||||||
self.multiworld.random.shuffle(valid_pool)
|
self.random.shuffle(valid_pool)
|
||||||
self.custom_recipes = {"rocket-part": Recipe("rocket-part", original_rocket_part.category,
|
self.custom_recipes = {"rocket-part": Recipe("rocket-part", original_rocket_part.category,
|
||||||
{valid_pool[x]: 10 for x in range(3 + ingredients_offset)},
|
{valid_pool[x]: 10 for x in range(3 + ingredients_offset)},
|
||||||
original_rocket_part.products,
|
original_rocket_part.products,
|
||||||
original_rocket_part.energy)}
|
original_rocket_part.energy)}
|
||||||
|
|
||||||
if self.multiworld.recipe_ingredients[self.player]:
|
if self.options.recipe_ingredients:
|
||||||
valid_pool = []
|
valid_pool = []
|
||||||
for pack in self.multiworld.max_science_pack[self.player].get_ordered_science_packs():
|
for pack in self.options.max_science_pack.get_ordered_science_packs():
|
||||||
valid_pool += sorted(science_pack_pools[pack])
|
valid_pool += sorted(science_pack_pools[pack])
|
||||||
self.multiworld.random.shuffle(valid_pool)
|
self.random.shuffle(valid_pool)
|
||||||
if pack in recipes: # skips over space science pack
|
if pack in recipes: # skips over space science pack
|
||||||
new_recipe = self.make_quick_recipe(recipes[pack], valid_pool, ingredients_offset=
|
new_recipe = self.make_quick_recipe(recipes[pack], valid_pool, ingredients_offset=
|
||||||
ingredients_offset)
|
ingredients_offset.value)
|
||||||
self.custom_recipes[pack] = new_recipe
|
self.custom_recipes[pack] = new_recipe
|
||||||
|
|
||||||
if self.multiworld.silo[self.player].value == Silo.option_randomize_recipe \
|
if self.options.silo.value == Silo.option_randomize_recipe \
|
||||||
or self.multiworld.satellite[self.player].value == Satellite.option_randomize_recipe:
|
or self.options.satellite.value == Satellite.option_randomize_recipe:
|
||||||
valid_pool = set()
|
valid_pool = set()
|
||||||
for pack in sorted(self.multiworld.max_science_pack[self.player].get_allowed_packs()):
|
for pack in sorted(self.options.max_science_pack.get_allowed_packs()):
|
||||||
valid_pool |= science_pack_pools[pack]
|
valid_pool |= science_pack_pools[pack]
|
||||||
|
|
||||||
if self.multiworld.silo[self.player].value == Silo.option_randomize_recipe:
|
if self.options.silo.value == Silo.option_randomize_recipe:
|
||||||
new_recipe = self.make_balanced_recipe(
|
new_recipe = self.make_balanced_recipe(
|
||||||
recipes["rocket-silo"], valid_pool,
|
recipes["rocket-silo"], valid_pool,
|
||||||
factor=(self.multiworld.max_science_pack[self.player].value + 1) / 7,
|
factor=(self.options.max_science_pack.value + 1) / 7,
|
||||||
ingredients_offset=ingredients_offset)
|
ingredients_offset=ingredients_offset.value)
|
||||||
self.custom_recipes["rocket-silo"] = new_recipe
|
self.custom_recipes["rocket-silo"] = new_recipe
|
||||||
|
|
||||||
if self.multiworld.satellite[self.player].value == Satellite.option_randomize_recipe:
|
if self.options.satellite.value == Satellite.option_randomize_recipe:
|
||||||
new_recipe = self.make_balanced_recipe(
|
new_recipe = self.make_balanced_recipe(
|
||||||
recipes["satellite"], valid_pool,
|
recipes["satellite"], valid_pool,
|
||||||
factor=(self.multiworld.max_science_pack[self.player].value + 1) / 7,
|
factor=(self.options.max_science_pack.value + 1) / 7,
|
||||||
ingredients_offset=ingredients_offset)
|
ingredients_offset=ingredients_offset.value)
|
||||||
self.custom_recipes["satellite"] = new_recipe
|
self.custom_recipes["satellite"] = new_recipe
|
||||||
bridge = "ap-energy-bridge"
|
bridge = "ap-energy-bridge"
|
||||||
new_recipe = self.make_quick_recipe(
|
new_recipe = self.make_quick_recipe(
|
||||||
Recipe(bridge, "crafting", {"replace_1": 1, "replace_2": 1, "replace_3": 1,
|
Recipe(bridge, "crafting", {"replace_1": 1, "replace_2": 1, "replace_3": 1,
|
||||||
"replace_4": 1, "replace_5": 1, "replace_6": 1},
|
"replace_4": 1, "replace_5": 1, "replace_6": 1},
|
||||||
{bridge: 1}, 10),
|
{bridge: 1}, 10),
|
||||||
sorted(science_pack_pools[self.multiworld.max_science_pack[self.player].get_ordered_science_packs()[0]]),
|
sorted(science_pack_pools[self.options.max_science_pack.get_ordered_science_packs()[0]]),
|
||||||
ingredients_offset=ingredients_offset)
|
ingredients_offset=ingredients_offset.value)
|
||||||
for ingredient_name in new_recipe.ingredients:
|
for ingredient_name in new_recipe.ingredients:
|
||||||
new_recipe.ingredients[ingredient_name] = self.multiworld.random.randint(50, 500)
|
new_recipe.ingredients[ingredient_name] = self.random.randint(50, 500)
|
||||||
self.custom_recipes[bridge] = new_recipe
|
self.custom_recipes[bridge] = new_recipe
|
||||||
|
|
||||||
needed_recipes = self.multiworld.max_science_pack[self.player].get_allowed_packs() | {"rocket-part"}
|
needed_recipes = self.options.max_science_pack.get_allowed_packs() | {"rocket-part"}
|
||||||
if self.multiworld.silo[self.player] != Silo.option_spawn:
|
if self.options.silo != Silo.option_spawn:
|
||||||
needed_recipes |= {"rocket-silo"}
|
needed_recipes |= {"rocket-silo"}
|
||||||
if self.multiworld.goal[self.player].value == Goal.option_satellite:
|
if self.options.goal.value == Goal.option_satellite:
|
||||||
needed_recipes |= {"satellite"}
|
needed_recipes |= {"satellite"}
|
||||||
|
|
||||||
for recipe in needed_recipes:
|
for recipe in needed_recipes:
|
||||||
@@ -542,7 +540,8 @@ class FactorioScienceLocation(FactorioLocation):
|
|||||||
|
|
||||||
self.ingredients = {Factorio.ordered_science_packs[self.complexity]: 1}
|
self.ingredients = {Factorio.ordered_science_packs[self.complexity]: 1}
|
||||||
for complexity in range(self.complexity):
|
for complexity in range(self.complexity):
|
||||||
if parent.multiworld.tech_cost_mix[self.player] > parent.multiworld.random.randint(0, 99):
|
if (parent.multiworld.worlds[self.player].options.tech_cost_mix >
|
||||||
|
parent.multiworld.worlds[self.player].random.randint(0, 99)):
|
||||||
self.ingredients[Factorio.ordered_science_packs[complexity]] = 1
|
self.ingredients[Factorio.ordered_science_packs[complexity]] = 1
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|||||||
@@ -22,9 +22,9 @@ enabled (opt-in).
|
|||||||
* You can add the necessary plando modules for your settings to the `requires` section of your YAML. Doing so will throw an error if the options that you need to generate properly are not enabled to ensure you will get the results you desire. Only enter in the plando modules that you are using here but it should look like:
|
* You can add the necessary plando modules for your settings to the `requires` section of your YAML. Doing so will throw an error if the options that you need to generate properly are not enabled to ensure you will get the results you desire. Only enter in the plando modules that you are using here but it should look like:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
requires:
|
requires:
|
||||||
version: current.version.number
|
version: current.version.number
|
||||||
plando: bosses, items, texts, connections
|
plando: bosses, items, texts, connections
|
||||||
```
|
```
|
||||||
|
|
||||||
## Item Plando
|
## Item Plando
|
||||||
@@ -74,77 +74,77 @@ A list of all available items and locations can be found in the [website's datap
|
|||||||
### Examples
|
### Examples
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
plando_items:
|
plando_items:
|
||||||
# example block 1 - Timespinner
|
# example block 1 - Timespinner
|
||||||
- item:
|
- item:
|
||||||
Empire Orb: 1
|
Empire Orb: 1
|
||||||
Radiant Orb: 1
|
Radiant Orb: 1
|
||||||
location: Starter Chest 1
|
location: Starter Chest 1
|
||||||
from_pool: true
|
from_pool: true
|
||||||
world: true
|
world: true
|
||||||
percentage: 50
|
percentage: 50
|
||||||
|
|
||||||
# example block 2 - Ocarina of Time
|
# example block 2 - Ocarina of Time
|
||||||
- items:
|
- items:
|
||||||
Kokiri Sword: 1
|
Kokiri Sword: 1
|
||||||
Biggoron Sword: 1
|
Biggoron Sword: 1
|
||||||
Bow: 1
|
Bow: 1
|
||||||
Magic Meter: 1
|
Magic Meter: 1
|
||||||
Progressive Strength Upgrade: 3
|
Progressive Strength Upgrade: 3
|
||||||
Progressive Hookshot: 2
|
Progressive Hookshot: 2
|
||||||
locations:
|
locations:
|
||||||
- Deku Tree Slingshot Chest
|
- Deku Tree Slingshot Chest
|
||||||
- Dodongos Cavern Bomb Bag Chest
|
- Dodongos Cavern Bomb Bag Chest
|
||||||
- Jabu Jabus Belly Boomerang Chest
|
- Jabu Jabus Belly Boomerang Chest
|
||||||
- Bottom of the Well Lens of Truth Chest
|
- Bottom of the Well Lens of Truth Chest
|
||||||
- Forest Temple Bow Chest
|
- Forest Temple Bow Chest
|
||||||
- Fire Temple Megaton Hammer Chest
|
- Fire Temple Megaton Hammer Chest
|
||||||
- Water Temple Longshot Chest
|
- Water Temple Longshot Chest
|
||||||
- Shadow Temple Hover Boots Chest
|
- Shadow Temple Hover Boots Chest
|
||||||
- Spirit Temple Silver Gauntlets Chest
|
- Spirit Temple Silver Gauntlets Chest
|
||||||
world: false
|
world: false
|
||||||
|
|
||||||
# example block 3 - Slay the Spire
|
# example block 3 - Slay the Spire
|
||||||
- items:
|
- items:
|
||||||
Boss Relic: 3
|
Boss Relic: 3
|
||||||
locations:
|
locations:
|
||||||
- Boss Relic 1
|
- Boss Relic 1
|
||||||
- Boss Relic 2
|
- Boss Relic 2
|
||||||
- Boss Relic 3
|
- Boss Relic 3
|
||||||
|
|
||||||
# example block 4 - Factorio
|
# example block 4 - Factorio
|
||||||
- items:
|
- items:
|
||||||
progressive-electric-energy-distribution: 2
|
progressive-electric-energy-distribution: 2
|
||||||
electric-energy-accumulators: 1
|
electric-energy-accumulators: 1
|
||||||
progressive-turret: 2
|
progressive-turret: 2
|
||||||
locations:
|
locations:
|
||||||
- military
|
- military
|
||||||
- gun-turret
|
- gun-turret
|
||||||
- logistic-science-pack
|
- logistic-science-pack
|
||||||
- steel-processing
|
- steel-processing
|
||||||
percentage: 80
|
percentage: 80
|
||||||
force: true
|
force: true
|
||||||
|
|
||||||
# example block 5 - Secret of Evermore
|
# example block 5 - Secret of Evermore
|
||||||
- items:
|
- items:
|
||||||
Levitate: 1
|
Levitate: 1
|
||||||
Revealer: 1
|
Revealer: 1
|
||||||
Energize: 1
|
Energize: 1
|
||||||
locations:
|
locations:
|
||||||
- Master Sword Pedestal
|
- Master Sword Pedestal
|
||||||
- Boss Relic 1
|
- Boss Relic 1
|
||||||
world: true
|
world: true
|
||||||
count: 2
|
count: 2
|
||||||
|
|
||||||
# example block 6 - A Link to the Past
|
# example block 6 - A Link to the Past
|
||||||
- items:
|
- items:
|
||||||
Progressive Sword: 4
|
Progressive Sword: 4
|
||||||
world:
|
world:
|
||||||
- BobsSlaytheSpire
|
- BobsSlaytheSpire
|
||||||
- BobsRogueLegacy
|
- BobsRogueLegacy
|
||||||
count:
|
count:
|
||||||
min: 1
|
min: 1
|
||||||
max: 4
|
max: 4
|
||||||
```
|
```
|
||||||
1. This block has a 50% chance to occur, and if it does, it will place either the Empire Orb or Radiant Orb on another
|
1. This block has a 50% chance to occur, and if it does, it will place either the Empire Orb or Radiant Orb on another
|
||||||
player's Starter Chest 1 and removes the chosen item from the item pool.
|
player's Starter Chest 1 and removes the chosen item from the item pool.
|
||||||
@@ -221,25 +221,25 @@ its [plando guide](/tutorial/A%20Link%20to%20the%20Past/plando/en#connections).
|
|||||||
### Examples
|
### Examples
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
plando_connections:
|
plando_connections:
|
||||||
# example block 1 - A Link to the Past
|
# example block 1 - A Link to the Past
|
||||||
- entrance: Cave Shop (Lake Hylia)
|
- entrance: Cave Shop (Lake Hylia)
|
||||||
exit: Cave 45
|
exit: Cave 45
|
||||||
direction: entrance
|
direction: entrance
|
||||||
- entrance: Cave 45
|
- entrance: Cave 45
|
||||||
exit: Cave Shop (Lake Hylia)
|
exit: Cave Shop (Lake Hylia)
|
||||||
direction: entrance
|
direction: entrance
|
||||||
- entrance: Agahnims Tower
|
- entrance: Agahnims Tower
|
||||||
exit: Old Man Cave Exit (West)
|
exit: Old Man Cave Exit (West)
|
||||||
direction: exit
|
direction: exit
|
||||||
|
|
||||||
# example block 2 - Minecraft
|
# example block 2 - Minecraft
|
||||||
- entrance: Overworld Structure 1
|
- entrance: Overworld Structure 1
|
||||||
exit: Nether Fortress
|
exit: Nether Fortress
|
||||||
direction: both
|
direction: both
|
||||||
- entrance: Overworld Structure 2
|
- entrance: Overworld Structure 2
|
||||||
exit: Village
|
exit: Village
|
||||||
direction: both
|
direction: both
|
||||||
```
|
```
|
||||||
|
|
||||||
1. These connections are decoupled, so going into the Lake Hylia Cave Shop will take you to the inside of Cave 45, and
|
1. These connections are decoupled, so going into the Lake Hylia Cave Shop will take you to the inside of Cave 45, and
|
||||||
|
|||||||
@@ -21,6 +21,16 @@ from .Charms import names as charm_names
|
|||||||
from BaseClasses import Region, Location, MultiWorld, Item, LocationProgressType, Tutorial, ItemClassification, CollectionState
|
from BaseClasses import Region, Location, MultiWorld, Item, LocationProgressType, Tutorial, ItemClassification, CollectionState
|
||||||
from worlds.AutoWorld import World, LogicMixin, WebWorld
|
from worlds.AutoWorld import World, LogicMixin, WebWorld
|
||||||
|
|
||||||
|
from settings import Group, Bool
|
||||||
|
|
||||||
|
|
||||||
|
class HollowKnightSettings(Group):
|
||||||
|
class DisableMapModSpoilers(Bool):
|
||||||
|
"""Disallows the APMapMod from showing spoiler placements."""
|
||||||
|
|
||||||
|
disable_spoilers: typing.Union[DisableMapModSpoilers, bool] = False
|
||||||
|
|
||||||
|
|
||||||
path_of_pain_locations = {
|
path_of_pain_locations = {
|
||||||
"Soul_Totem-Path_of_Pain_Below_Thornskip",
|
"Soul_Totem-Path_of_Pain_Below_Thornskip",
|
||||||
"Lore_Tablet-Path_of_Pain_Entrance",
|
"Lore_Tablet-Path_of_Pain_Entrance",
|
||||||
@@ -124,14 +134,25 @@ shop_cost_types: typing.Dict[str, typing.Tuple[str, ...]] = {
|
|||||||
|
|
||||||
|
|
||||||
class HKWeb(WebWorld):
|
class HKWeb(WebWorld):
|
||||||
tutorials = [Tutorial(
|
setup_en = Tutorial(
|
||||||
"Mod Setup and Use Guide",
|
"Mod Setup and Use Guide",
|
||||||
"A guide to playing Hollow Knight with Archipelago.",
|
"A guide to playing Hollow Knight with Archipelago.",
|
||||||
"English",
|
"English",
|
||||||
"setup_en.md",
|
"setup_en.md",
|
||||||
"setup/en",
|
"setup/en",
|
||||||
["Ijwu"]
|
["Ijwu"]
|
||||||
)]
|
)
|
||||||
|
|
||||||
|
setup_pt_br = Tutorial(
|
||||||
|
setup_en.tutorial_name,
|
||||||
|
setup_en.description,
|
||||||
|
"Português Brasileiro",
|
||||||
|
"setup_pt_br.md",
|
||||||
|
"setup/pt_br",
|
||||||
|
["JoaoVictor-FA"]
|
||||||
|
)
|
||||||
|
|
||||||
|
tutorials = [setup_en, setup_pt_br]
|
||||||
|
|
||||||
bug_report_page = "https://github.com/Ijwu/Archipelago.HollowKnight/issues/new?assignees=&labels=bug%2C+needs+investigation&template=bug_report.md&title="
|
bug_report_page = "https://github.com/Ijwu/Archipelago.HollowKnight/issues/new?assignees=&labels=bug%2C+needs+investigation&template=bug_report.md&title="
|
||||||
|
|
||||||
@@ -145,6 +166,7 @@ class HKWorld(World):
|
|||||||
game: str = "Hollow Knight"
|
game: str = "Hollow Knight"
|
||||||
options_dataclass = HKOptions
|
options_dataclass = HKOptions
|
||||||
options: HKOptions
|
options: HKOptions
|
||||||
|
settings: typing.ClassVar[HollowKnightSettings]
|
||||||
|
|
||||||
web = HKWeb()
|
web = HKWeb()
|
||||||
|
|
||||||
@@ -512,26 +534,16 @@ class HKWorld(World):
|
|||||||
for option_name in hollow_knight_options:
|
for option_name in hollow_knight_options:
|
||||||
option = getattr(self.options, option_name)
|
option = getattr(self.options, option_name)
|
||||||
try:
|
try:
|
||||||
|
# exclude more complex types - we only care about int, bool, enum for player options; the client
|
||||||
|
# can get them back to the necessary type.
|
||||||
optionvalue = int(option.value)
|
optionvalue = int(option.value)
|
||||||
except TypeError:
|
|
||||||
pass # C# side is currently typed as dict[str, int], drop what doesn't fit
|
|
||||||
else:
|
|
||||||
options[option_name] = optionvalue
|
options[option_name] = optionvalue
|
||||||
|
except TypeError:
|
||||||
|
pass
|
||||||
|
|
||||||
# 32 bit int
|
# 32 bit int
|
||||||
slot_data["seed"] = self.random.randint(-2147483647, 2147483646)
|
slot_data["seed"] = self.random.randint(-2147483647, 2147483646)
|
||||||
|
|
||||||
# Backwards compatibility for shop cost data (HKAP < 0.1.0)
|
|
||||||
if not self.options.CostSanity:
|
|
||||||
for shop, terms in shop_cost_types.items():
|
|
||||||
unit = cost_terms[next(iter(terms))].option
|
|
||||||
if unit == "Geo":
|
|
||||||
continue
|
|
||||||
slot_data[f"{unit}_costs"] = {
|
|
||||||
loc.name: next(iter(loc.costs.values()))
|
|
||||||
for loc in self.created_multi_locations[shop]
|
|
||||||
}
|
|
||||||
|
|
||||||
# HKAP 0.1.0 and later cost data.
|
# HKAP 0.1.0 and later cost data.
|
||||||
location_costs = {}
|
location_costs = {}
|
||||||
for region in self.multiworld.get_regions(self.player):
|
for region in self.multiworld.get_regions(self.player):
|
||||||
@@ -544,6 +556,8 @@ class HKWorld(World):
|
|||||||
|
|
||||||
slot_data["grub_count"] = self.grub_count
|
slot_data["grub_count"] = self.grub_count
|
||||||
|
|
||||||
|
slot_data["is_race"] = self.settings.disable_spoilers or self.multiworld.is_race
|
||||||
|
|
||||||
return slot_data
|
return slot_data
|
||||||
|
|
||||||
def create_item(self, name: str) -> HKItem:
|
def create_item(self, name: str) -> HKItem:
|
||||||
@@ -601,11 +615,11 @@ class HKWorld(World):
|
|||||||
if change:
|
if change:
|
||||||
for effect_name, effect_value in item_effects.get(item.name, {}).items():
|
for effect_name, effect_value in item_effects.get(item.name, {}).items():
|
||||||
state.prog_items[item.player][effect_name] += effect_value
|
state.prog_items[item.player][effect_name] += effect_value
|
||||||
if item.name in {"Left_Mothwing_Cloak", "Right_Mothwing_Cloak"}:
|
if item.name in {"Left_Mothwing_Cloak", "Right_Mothwing_Cloak"}:
|
||||||
if state.prog_items[item.player].get('RIGHTDASH', 0) and \
|
if state.prog_items[item.player].get('RIGHTDASH', 0) and \
|
||||||
state.prog_items[item.player].get('LEFTDASH', 0):
|
state.prog_items[item.player].get('LEFTDASH', 0):
|
||||||
(state.prog_items[item.player]["RIGHTDASH"], state.prog_items[item.player]["LEFTDASH"]) = \
|
(state.prog_items[item.player]["RIGHTDASH"], state.prog_items[item.player]["LEFTDASH"]) = \
|
||||||
([max(state.prog_items[item.player]["RIGHTDASH"], state.prog_items[item.player]["LEFTDASH"])] * 2)
|
([max(state.prog_items[item.player]["RIGHTDASH"], state.prog_items[item.player]["LEFTDASH"])] * 2)
|
||||||
return change
|
return change
|
||||||
|
|
||||||
def remove(self, state, item: HKItem) -> bool:
|
def remove(self, state, item: HKItem) -> bool:
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
### What to do if Lumafly fails to find your installation directory
|
### What to do if Lumafly fails to find your installation directory
|
||||||
1. Find the directory manually.
|
1. Find the directory manually.
|
||||||
* Xbox Game Pass:
|
* Xbox Game Pass:
|
||||||
1. Enter the XBox app and move your mouse over "Hollow Knight" on the left sidebar.
|
1. Enter the Xbox app and move your mouse over "Hollow Knight" on the left sidebar.
|
||||||
2. Click the three points then click "Manage".
|
2. Click the three points then click "Manage".
|
||||||
3. Go to the "Files" tab and select "Browse...".
|
3. Go to the "Files" tab and select "Browse...".
|
||||||
4. Click "Hollow Knight", then "Content", then click the path bar and copy it.
|
4. Click "Hollow Knight", then "Content", then click the path bar and copy it.
|
||||||
|
|||||||
52
worlds/hk/docs/setup_pt_br.md
Normal file
52
worlds/hk/docs/setup_pt_br.md
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
# Guia de configuração para Hollow Knight no Archipelago
|
||||||
|
|
||||||
|
## Programas obrigatórios
|
||||||
|
* Baixe e extraia o Lumafly Mod Manager (gerenciador de mods Lumafly) do [Site Lumafly](https://themulhima.github.io/Lumafly/).
|
||||||
|
* Uma cópia legal de Hollow Knight.
|
||||||
|
* Versões Steam, Gog, e Xbox Game Pass do jogo são suportadas.
|
||||||
|
* Windows, Mac, e Linux (incluindo Steam Deck) são suportados.
|
||||||
|
|
||||||
|
## Instalando o mod Archipelago Mod usando Lumafly
|
||||||
|
1. Abra o Lumafly e confirme que ele localizou sua pasta de instalação do Hollow Knight.
|
||||||
|
2. Clique em "Install (instalar)" perto da opção "Archipelago" mod.
|
||||||
|
* Se quiser, instale também o "Archipelago Map Mod (mod do mapa do archipelago)" para usá-lo como rastreador dentro do jogo.
|
||||||
|
3. Abra o jogo, tudo preparado!
|
||||||
|
|
||||||
|
### O que fazer se o Lumafly falha em encontrar a sua pasta de instalação
|
||||||
|
1. Encontre a pasta manualmente.
|
||||||
|
* Xbox Game Pass:
|
||||||
|
1. Entre no seu aplicativo Xbox e mova seu mouse em cima de "Hollow Knight" na sua barra da esquerda.
|
||||||
|
2. Clique nos 3 pontos depois clique gerenciar.
|
||||||
|
3. Vá nos arquivos e selecione procurar.
|
||||||
|
4. Clique em "Hollow Knight", depois em "Content (Conteúdo)", depois clique na barra com o endereço e a copie.
|
||||||
|
* Steam:
|
||||||
|
1. Você provavelmente colocou sua biblioteca Steam num local não padrão. Se esse for o caso você provavelmente sabe onde está.
|
||||||
|
. Encontre sua biblioteca Steam, depois encontre a pasta do Hollow Knight e copie seu endereço.
|
||||||
|
* Windows - `C:\Program Files (x86)\Steam\steamapps\common\Hollow Knight`
|
||||||
|
* Linux/Steam Deck - `~/.local/share/Steam/steamapps/common/Hollow Knight`
|
||||||
|
* Mac - `~/Library/Application Support/Steam/steamapps/common/Hollow Knight/hollow_knight.app`
|
||||||
|
2. Rode o Lumafly como administrador e, quando ele perguntar pelo endereço do arquivo, cole o endereço do arquivo que você copiou.
|
||||||
|
|
||||||
|
## Configurando seu arquivo YAML
|
||||||
|
### O que é um YAML e por que eu preciso de um?
|
||||||
|
Um arquivo YAML é a forma que você informa suas configurações do jogador para o Archipelago.
|
||||||
|
Olhe o [guia de configuração básica de um multiworld](/tutorial/Archipelago/setup/en) aqui no site do Archipelago para aprender mais.
|
||||||
|
|
||||||
|
### Onde eu consigo o YAML?
|
||||||
|
Você pode usar a [página de configurações do jogador para Hollow Knight](/games/Hollow%20Knight/player-options) aqui no site do Archipelago
|
||||||
|
para gerar o YAML usando a interface gráfica.
|
||||||
|
|
||||||
|
### Entrando numa partida de Archipelago no Hollow Knight
|
||||||
|
1. Começe o jogo depois de instalar todos os mods necessários.
|
||||||
|
2. Crie um **novo jogo salvo.**
|
||||||
|
3. Selecione o modo de jogo **Archipelago** do menu de seleção.
|
||||||
|
4. Coloque as configurações corretas do seu servidor Archipelago.
|
||||||
|
5. Aperte em **Começar**. O jogo vai travar por uns segundos enquanto ele coloca todos itens.
|
||||||
|
6. O jogo vai te colocar imediatamente numa partida randomizada.
|
||||||
|
* Se você está esperando uma contagem então espere ele cair antes de apertar começar.
|
||||||
|
* Ou clique em começar e pause o jogo enquanto estiver nele.
|
||||||
|
|
||||||
|
## Dicas e outros comandos
|
||||||
|
Enquanto jogar um multiworld, você pode interagir com o servidor usando vários comandos listados no
|
||||||
|
[Guia de comandos](/tutorial/Archipelago/commands/en). Você pode usar o cliente de texto do Archipelago para isso,
|
||||||
|
que está incluido na ultima versão do [Archipelago software](https://github.com/ArchipelagoMW/Archipelago/releases/latest).
|
||||||
@@ -325,7 +325,7 @@ class KDL3World(World):
|
|||||||
|
|
||||||
def generate_output(self, output_directory: str) -> None:
|
def generate_output(self, output_directory: str) -> None:
|
||||||
try:
|
try:
|
||||||
patch = KDL3ProcedurePatch()
|
patch = KDL3ProcedurePatch(player=self.player, player_name=self.player_name)
|
||||||
patch_rom(self, patch)
|
patch_rom(self, patch)
|
||||||
|
|
||||||
self.rom_name = patch.name
|
self.rom_name = patch.name
|
||||||
|
|||||||
@@ -31,6 +31,9 @@ def check_stdin() -> None:
|
|||||||
print("WARNING: Console input is not routed reliably on Windows, use the GUI instead.")
|
print("WARNING: Console input is not routed reliably on Windows, use the GUI instead.")
|
||||||
|
|
||||||
class KH1ClientCommandProcessor(ClientCommandProcessor):
|
class KH1ClientCommandProcessor(ClientCommandProcessor):
|
||||||
|
def __init__(self, ctx):
|
||||||
|
super().__init__(ctx)
|
||||||
|
|
||||||
def _cmd_deathlink(self):
|
def _cmd_deathlink(self):
|
||||||
"""Toggles Deathlink"""
|
"""Toggles Deathlink"""
|
||||||
global death_link
|
global death_link
|
||||||
@@ -40,6 +43,40 @@ class KH1ClientCommandProcessor(ClientCommandProcessor):
|
|||||||
else:
|
else:
|
||||||
death_link = True
|
death_link = True
|
||||||
self.output(f"Death Link turned on")
|
self.output(f"Death Link turned on")
|
||||||
|
|
||||||
|
def _cmd_goal(self):
|
||||||
|
"""Prints goal setting"""
|
||||||
|
if "goal" in self.ctx.slot_data.keys():
|
||||||
|
self.output(str(self.ctx.slot_data["goal"]))
|
||||||
|
else:
|
||||||
|
self.output("Unknown")
|
||||||
|
|
||||||
|
def _cmd_eotw_unlock(self):
|
||||||
|
"""Prints End of the World Unlock setting"""
|
||||||
|
if "required_reports_door" in self.ctx.slot_data.keys():
|
||||||
|
if self.ctx.slot_data["required_reports_door"] > 13:
|
||||||
|
self.output("Item")
|
||||||
|
else:
|
||||||
|
self.output(str(self.ctx.slot_data["required_reports_eotw"]) + " reports")
|
||||||
|
else:
|
||||||
|
self.output("Unknown")
|
||||||
|
|
||||||
|
def _cmd_door_unlock(self):
|
||||||
|
"""Prints Final Rest Door Unlock setting"""
|
||||||
|
if "door" in self.ctx.slot_data.keys():
|
||||||
|
if self.ctx.slot_data["door"] == "reports":
|
||||||
|
self.output(str(self.ctx.slot_data["required_reports_door"]) + " reports")
|
||||||
|
else:
|
||||||
|
self.output(str(self.ctx.slot_data["door"]))
|
||||||
|
else:
|
||||||
|
self.output("Unknown")
|
||||||
|
|
||||||
|
def _cmd_advanced_logic(self):
|
||||||
|
"""Prints advanced logic setting"""
|
||||||
|
if "advanced_logic" in self.ctx.slot_data.keys():
|
||||||
|
self.output(str(self.ctx.slot_data["advanced_logic"]))
|
||||||
|
else:
|
||||||
|
self.output("Unknown")
|
||||||
|
|
||||||
class KH1Context(CommonContext):
|
class KH1Context(CommonContext):
|
||||||
command_processor: int = KH1ClientCommandProcessor
|
command_processor: int = KH1ClientCommandProcessor
|
||||||
@@ -51,6 +88,8 @@ class KH1Context(CommonContext):
|
|||||||
self.send_index: int = 0
|
self.send_index: int = 0
|
||||||
self.syncing = False
|
self.syncing = False
|
||||||
self.awaiting_bridge = False
|
self.awaiting_bridge = False
|
||||||
|
self.hinted_synth_location_ids = False
|
||||||
|
self.slot_data = {}
|
||||||
# self.game_communication_path: files go in this path to pass data between us and the actual game
|
# self.game_communication_path: files go in this path to pass data between us and the actual game
|
||||||
if "localappdata" in os.environ:
|
if "localappdata" in os.environ:
|
||||||
self.game_communication_path = os.path.expandvars(r"%localappdata%/KH1FM")
|
self.game_communication_path = os.path.expandvars(r"%localappdata%/KH1FM")
|
||||||
@@ -104,6 +143,7 @@ class KH1Context(CommonContext):
|
|||||||
f.close()
|
f.close()
|
||||||
|
|
||||||
#Handle Slot Data
|
#Handle Slot Data
|
||||||
|
self.slot_data = args['slot_data']
|
||||||
for key in list(args['slot_data'].keys()):
|
for key in list(args['slot_data'].keys()):
|
||||||
with open(os.path.join(self.game_communication_path, key + ".cfg"), 'w') as f:
|
with open(os.path.join(self.game_communication_path, key + ".cfg"), 'w') as f:
|
||||||
f.write(str(args['slot_data'][key]))
|
f.write(str(args['slot_data'][key]))
|
||||||
@@ -217,11 +257,13 @@ async def game_watcher(ctx: KH1Context):
|
|||||||
if timegm(time.strptime(st, '%Y%m%d%H%M%S')) > ctx.last_death_link and int(time.time()) % int(timegm(time.strptime(st, '%Y%m%d%H%M%S'))) < 10:
|
if timegm(time.strptime(st, '%Y%m%d%H%M%S')) > ctx.last_death_link and int(time.time()) % int(timegm(time.strptime(st, '%Y%m%d%H%M%S'))) < 10:
|
||||||
await ctx.send_death(death_text = "Sora was defeated!")
|
await ctx.send_death(death_text = "Sora was defeated!")
|
||||||
if file.find("insynthshop") > -1:
|
if file.find("insynthshop") > -1:
|
||||||
await ctx.send_msgs([{
|
if not ctx.hinted_synth_location_ids:
|
||||||
"cmd": "LocationScouts",
|
await ctx.send_msgs([{
|
||||||
"locations": [2656401,2656402,2656403,2656404,2656405,2656406],
|
"cmd": "LocationScouts",
|
||||||
"create_as_hint": 2
|
"locations": [2656401,2656402,2656403,2656404,2656405,2656406],
|
||||||
}])
|
"create_as_hint": 2
|
||||||
|
}])
|
||||||
|
ctx.hinted_synth_location_ids = True
|
||||||
ctx.locations_checked = sending
|
ctx.locations_checked = sending
|
||||||
message = [{"cmd": 'LocationChecks', "locations": sending}]
|
message = [{"cmd": 'LocationChecks', "locations": sending}]
|
||||||
await ctx.send_msgs(message)
|
await ctx.send_msgs(message)
|
||||||
|
|||||||
@@ -101,7 +101,18 @@ class KH2World(World):
|
|||||||
if ability in self.goofy_ability_dict and self.goofy_ability_dict[ability] >= 1:
|
if ability in self.goofy_ability_dict and self.goofy_ability_dict[ability] >= 1:
|
||||||
self.goofy_ability_dict[ability] -= 1
|
self.goofy_ability_dict[ability] -= 1
|
||||||
|
|
||||||
slot_data = self.options.as_dict("Goal", "FinalXemnas", "LuckyEmblemsRequired", "BountyRequired")
|
slot_data = self.options.as_dict(
|
||||||
|
"Goal",
|
||||||
|
"FinalXemnas",
|
||||||
|
"LuckyEmblemsRequired",
|
||||||
|
"BountyRequired",
|
||||||
|
"FightLogic",
|
||||||
|
"FinalFormLogic",
|
||||||
|
"AutoFormLogic",
|
||||||
|
"LevelDepth",
|
||||||
|
"DonaldGoofyStatsanity",
|
||||||
|
"CorSkipToggle"
|
||||||
|
)
|
||||||
slot_data.update({
|
slot_data.update({
|
||||||
"hitlist": [], # remove this after next update
|
"hitlist": [], # remove this after next update
|
||||||
"PoptrackerVersionCheck": 4.3,
|
"PoptrackerVersionCheck": 4.3,
|
||||||
|
|||||||
@@ -83,8 +83,8 @@ class ItemName:
|
|||||||
RUPEES_200 = "200 Rupees"
|
RUPEES_200 = "200 Rupees"
|
||||||
RUPEES_500 = "500 Rupees"
|
RUPEES_500 = "500 Rupees"
|
||||||
SEASHELL = "Seashell"
|
SEASHELL = "Seashell"
|
||||||
MESSAGE = "Master Stalfos' Message"
|
MESSAGE = "Nothing"
|
||||||
GEL = "Gel"
|
GEL = "Zol Attack"
|
||||||
BOOMERANG = "Boomerang"
|
BOOMERANG = "Boomerang"
|
||||||
HEART_PIECE = "Heart Piece"
|
HEART_PIECE = "Heart Piece"
|
||||||
BOWWOW = "BowWow"
|
BOWWOW = "BowWow"
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ def fixGoldenLeaf(rom):
|
|||||||
rom.patch(0x03, 0x0980, ASM("ld a, [$DB15]"), ASM("ld a, [wGoldenLeaves]")) # If leaves >= 6 move richard
|
rom.patch(0x03, 0x0980, ASM("ld a, [$DB15]"), ASM("ld a, [wGoldenLeaves]")) # If leaves >= 6 move richard
|
||||||
rom.patch(0x06, 0x0059, ASM("ld a, [$DB15]"), ASM("ld a, [wGoldenLeaves]")) # If leaves >= 6 move richard
|
rom.patch(0x06, 0x0059, ASM("ld a, [$DB15]"), ASM("ld a, [wGoldenLeaves]")) # If leaves >= 6 move richard
|
||||||
rom.patch(0x06, 0x007D, ASM("ld a, [$DB15]"), ASM("ld a, [wGoldenLeaves]")) # Richard message if no leaves
|
rom.patch(0x06, 0x007D, ASM("ld a, [$DB15]"), ASM("ld a, [wGoldenLeaves]")) # Richard message if no leaves
|
||||||
rom.patch(0x06, 0x00B8, ASM("ld [$DB15], a"), ASM("ld [wGoldenLeaves], a")) # Stores FF in the leaf counter if we opened the path
|
rom.patch(0x06, 0x00B6, ASM("ld a, $FF"), ASM("ld a, $06"))
|
||||||
|
rom.patch(0x06, 0x00B8, ASM("ld [$DB15], a"), ASM("ld [wGoldenLeaves], a")) # Stores 6 in the leaf counter if we opened the path (instead of FF, so that nothing breaks if we get more for some reason)
|
||||||
# 6:40EE uses leaves == 6 to check if we have collected the key, but only to change the message.
|
# 6:40EE uses leaves == 6 to check if we have collected the key, but only to change the message.
|
||||||
# rom.patch(0x06, 0x2AEF, ASM("ld a, [$DB15]"), ASM("ld a, [wGoldenLeaves]")) # Telephone message handler
|
# rom.patch(0x06, 0x2AEF, ASM("ld a, [$DB15]"), ASM("ld a, [wGoldenLeaves]")) # Telephone message handler
|
||||||
|
|||||||
@@ -81,23 +81,23 @@ talking:
|
|||||||
|
|
||||||
; Give powder
|
; Give powder
|
||||||
ld a, [$DB4C]
|
ld a, [$DB4C]
|
||||||
cp $10
|
cp $20
|
||||||
jr nc, doNotGivePowder
|
jr nc, doNotGivePowder
|
||||||
ld a, $10
|
ld a, $20
|
||||||
ld [$DB4C], a
|
ld [$DB4C], a
|
||||||
doNotGivePowder:
|
doNotGivePowder:
|
||||||
|
|
||||||
ld a, [$DB4D]
|
ld a, [$DB4D]
|
||||||
cp $10
|
cp $30
|
||||||
jr nc, doNotGiveBombs
|
jr nc, doNotGiveBombs
|
||||||
ld a, $10
|
ld a, $30
|
||||||
ld [$DB4D], a
|
ld [$DB4D], a
|
||||||
doNotGiveBombs:
|
doNotGiveBombs:
|
||||||
|
|
||||||
ld a, [$DB45]
|
ld a, [$DB45]
|
||||||
cp $10
|
cp $30
|
||||||
jr nc, doNotGiveArrows
|
jr nc, doNotGiveArrows
|
||||||
ld a, $10
|
ld a, $30
|
||||||
ld [$DB45], a
|
ld [$DB45], a
|
||||||
doNotGiveArrows:
|
doNotGiveArrows:
|
||||||
|
|
||||||
|
|||||||
@@ -149,6 +149,8 @@ class MagpieBridge:
|
|||||||
item_tracker = None
|
item_tracker = None
|
||||||
ws = None
|
ws = None
|
||||||
features = []
|
features = []
|
||||||
|
slot_data = {}
|
||||||
|
|
||||||
async def handler(self, websocket):
|
async def handler(self, websocket):
|
||||||
self.ws = websocket
|
self.ws = websocket
|
||||||
while True:
|
while True:
|
||||||
@@ -163,6 +165,9 @@ class MagpieBridge:
|
|||||||
await self.send_all_inventory()
|
await self.send_all_inventory()
|
||||||
if "checks" in self.features:
|
if "checks" in self.features:
|
||||||
await self.send_all_checks()
|
await self.send_all_checks()
|
||||||
|
if "slot_data" in self.features:
|
||||||
|
await self.send_slot_data(self.slot_data)
|
||||||
|
|
||||||
# Translate renamed IDs back to LADXR IDs
|
# Translate renamed IDs back to LADXR IDs
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def fixup_id(the_id):
|
def fixup_id(the_id):
|
||||||
@@ -222,6 +227,18 @@ class MagpieBridge:
|
|||||||
return
|
return
|
||||||
await gps.send_location(self.ws)
|
await gps.send_location(self.ws)
|
||||||
|
|
||||||
|
async def send_slot_data(self, slot_data):
|
||||||
|
if not self.ws:
|
||||||
|
return
|
||||||
|
|
||||||
|
logger.debug("Sending slot_data to magpie.")
|
||||||
|
message = {
|
||||||
|
"type": "slot_data",
|
||||||
|
"slot_data": slot_data
|
||||||
|
}
|
||||||
|
|
||||||
|
await self.ws.send(json.dumps(message))
|
||||||
|
|
||||||
async def serve(self):
|
async def serve(self):
|
||||||
async with websockets.serve(lambda w: self.handler(w), "", 17026, logger=logger):
|
async with websockets.serve(lambda w: self.handler(w), "", 17026, logger=logger):
|
||||||
await asyncio.Future() # run forever
|
await asyncio.Future() # run forever
|
||||||
@@ -237,4 +254,3 @@ class MagpieBridge:
|
|||||||
await self.send_all_inventory()
|
await self.send_all_inventory()
|
||||||
else:
|
else:
|
||||||
await self.send_inventory_diffs()
|
await self.send_inventory_diffs()
|
||||||
|
|
||||||
|
|||||||
@@ -216,7 +216,7 @@ class LinksAwakeningWorld(World):
|
|||||||
for _ in range(count):
|
for _ in range(count):
|
||||||
if item_name in exclude:
|
if item_name in exclude:
|
||||||
exclude.remove(item_name) # this is destructive. create unique list above
|
exclude.remove(item_name) # this is destructive. create unique list above
|
||||||
self.multiworld.itempool.append(self.create_item("Master Stalfos' Message"))
|
self.multiworld.itempool.append(self.create_item("Nothing"))
|
||||||
else:
|
else:
|
||||||
item = self.create_item(item_name)
|
item = self.create_item(item_name)
|
||||||
|
|
||||||
@@ -512,3 +512,34 @@ class LinksAwakeningWorld(World):
|
|||||||
if change and item.name in self.rupees:
|
if change and item.name in self.rupees:
|
||||||
state.prog_items[self.player]["RUPEES"] -= self.rupees[item.name]
|
state.prog_items[self.player]["RUPEES"] -= self.rupees[item.name]
|
||||||
return change
|
return change
|
||||||
|
|
||||||
|
def get_filler_item_name(self) -> str:
|
||||||
|
return "Nothing"
|
||||||
|
|
||||||
|
def fill_slot_data(self):
|
||||||
|
slot_data = {}
|
||||||
|
|
||||||
|
if not self.multiworld.is_race:
|
||||||
|
# all of these option are NOT used by the LADX- or Text-Client.
|
||||||
|
# they are used by Magpie tracker (https://github.com/kbranch/Magpie/wiki/Autotracker-API)
|
||||||
|
# for convenient auto-tracking of the generated settings and adjusting the tracker accordingly
|
||||||
|
|
||||||
|
slot_options = ["instrument_count"]
|
||||||
|
|
||||||
|
slot_options_display_name = [
|
||||||
|
"goal", "logic", "tradequest", "rooster",
|
||||||
|
"experimental_dungeon_shuffle", "experimental_entrance_shuffle", "trendy_game", "gfxmod",
|
||||||
|
"shuffle_nightmare_keys", "shuffle_small_keys", "shuffle_maps",
|
||||||
|
"shuffle_compasses", "shuffle_stone_beaks", "shuffle_instruments", "nag_messages"
|
||||||
|
]
|
||||||
|
|
||||||
|
# use the default behaviour to grab options
|
||||||
|
slot_data = self.options.as_dict(*slot_options)
|
||||||
|
|
||||||
|
# for options which should not get the internal int value but the display name use the extra handling
|
||||||
|
slot_data.update({
|
||||||
|
option: value.current_key
|
||||||
|
for option, value in dataclasses.asdict(self.options).items() if option in slot_options_display_name
|
||||||
|
})
|
||||||
|
|
||||||
|
return slot_data
|
||||||
|
|||||||
@@ -482,7 +482,9 @@
|
|||||||
Crossroads:
|
Crossroads:
|
||||||
door: Crossroads Entrance
|
door: Crossroads Entrance
|
||||||
The Tenacious:
|
The Tenacious:
|
||||||
door: Tenacious Entrance
|
- door: Tenacious Entrance
|
||||||
|
- room: The Tenacious
|
||||||
|
door: Shortcut to Hub Room
|
||||||
Near Far Area: True
|
Near Far Area: True
|
||||||
Hedge Maze:
|
Hedge Maze:
|
||||||
door: Shortcut to Hedge Maze
|
door: Shortcut to Hedge Maze
|
||||||
|
|||||||
Binary file not shown.
@@ -19,7 +19,7 @@ from .shop import FIGURINES, PROG_SHOP_ITEMS, SHOP_ITEMS, USEFUL_SHOP_ITEMS, shu
|
|||||||
from .subclasses import MessengerEntrance, MessengerItem, MessengerRegion, MessengerShopLocation
|
from .subclasses import MessengerEntrance, MessengerItem, MessengerRegion, MessengerShopLocation
|
||||||
|
|
||||||
components.append(
|
components.append(
|
||||||
Component("The Messenger", component_type=Type.CLIENT, func=launch_game)#, game_name="The Messenger", supports_uri=True)
|
Component("The Messenger", component_type=Type.CLIENT, func=launch_game, game_name="The Messenger", supports_uri=True)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -27,6 +27,7 @@ class MessengerSettings(Group):
|
|||||||
class GamePath(FilePath):
|
class GamePath(FilePath):
|
||||||
description = "The Messenger game executable"
|
description = "The Messenger game executable"
|
||||||
is_exe = True
|
is_exe = True
|
||||||
|
md5s = ["1b53534569060bc06179356cd968ed1d"]
|
||||||
|
|
||||||
game_path: GamePath = GamePath("TheMessenger.exe")
|
game_path: GamePath = GamePath("TheMessenger.exe")
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
|
import argparse
|
||||||
import io
|
import io
|
||||||
import logging
|
import logging
|
||||||
import os.path
|
import os.path
|
||||||
import subprocess
|
import subprocess
|
||||||
import urllib.request
|
import urllib.request
|
||||||
from shutil import which
|
from shutil import which
|
||||||
from tkinter.messagebox import askyesnocancel
|
|
||||||
from typing import Any, Optional
|
from typing import Any, Optional
|
||||||
from zipfile import ZipFile
|
from zipfile import ZipFile
|
||||||
from Utils import open_file
|
from Utils import open_file
|
||||||
@@ -17,11 +17,33 @@ from Utils import is_windows, messagebox, tuplize_version
|
|||||||
MOD_URL = "https://api.github.com/repos/alwaysintreble/TheMessengerRandomizerModAP/releases/latest"
|
MOD_URL = "https://api.github.com/repos/alwaysintreble/TheMessengerRandomizerModAP/releases/latest"
|
||||||
|
|
||||||
|
|
||||||
def launch_game(url: Optional[str] = None) -> None:
|
def ask_yes_no_cancel(title: str, text: str) -> Optional[bool]:
|
||||||
|
"""
|
||||||
|
Wrapper for tkinter.messagebox.askyesnocancel, that creates a popup dialog box with yes, no, and cancel buttons.
|
||||||
|
|
||||||
|
:param title: Title to be displayed at the top of the message box.
|
||||||
|
:param text: Text to be displayed inside the message box.
|
||||||
|
:return: Returns True if yes, False if no, None if cancel.
|
||||||
|
"""
|
||||||
|
from tkinter import Tk, messagebox
|
||||||
|
root = Tk()
|
||||||
|
root.withdraw()
|
||||||
|
ret = messagebox.askyesnocancel(title, text)
|
||||||
|
root.update()
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def launch_game(*args) -> None:
|
||||||
"""Check the game installation, then launch it"""
|
"""Check the game installation, then launch it"""
|
||||||
def courier_installed() -> bool:
|
def courier_installed() -> bool:
|
||||||
"""Check if Courier is installed"""
|
"""Check if Courier is installed"""
|
||||||
return os.path.exists(os.path.join(game_folder, "TheMessenger_Data", "Managed", "Assembly-CSharp.Courier.mm.dll"))
|
assembly_path = os.path.join(game_folder, "TheMessenger_Data", "Managed", "Assembly-CSharp.dll")
|
||||||
|
with open(assembly_path, "rb") as assembly:
|
||||||
|
for line in assembly:
|
||||||
|
if b"Courier" in line:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
def mod_installed() -> bool:
|
def mod_installed() -> bool:
|
||||||
"""Check if the mod is installed"""
|
"""Check if the mod is installed"""
|
||||||
@@ -56,27 +78,34 @@ def launch_game(url: Optional[str] = None) -> None:
|
|||||||
if not is_windows:
|
if not is_windows:
|
||||||
mono_exe = which("mono")
|
mono_exe = which("mono")
|
||||||
if not mono_exe:
|
if not mono_exe:
|
||||||
# steam deck support but doesn't currently work
|
# download and use mono kickstart
|
||||||
messagebox("Failure", "Failed to install Courier", True)
|
# this allows steam deck support
|
||||||
raise RuntimeError("Failed to install Courier")
|
mono_kick_url = "https://github.com/flibitijibibo/MonoKickstart/archive/716f0a2bd5d75138969090494a76328f39a6dd78.zip"
|
||||||
# # download and use mono kickstart
|
files = []
|
||||||
# # this allows steam deck support
|
with urllib.request.urlopen(mono_kick_url) as download:
|
||||||
# mono_kick_url = "https://github.com/flibitijibibo/MonoKickstart/archive/refs/heads/master.zip"
|
with ZipFile(io.BytesIO(download.read()), "r") as zf:
|
||||||
# target = os.path.join(folder, "monoKickstart")
|
for member in zf.infolist():
|
||||||
# os.makedirs(target, exist_ok=True)
|
if "precompiled/" not in member.filename or member.filename.endswith("/"):
|
||||||
# with urllib.request.urlopen(mono_kick_url) as download:
|
continue
|
||||||
# with ZipFile(io.BytesIO(download.read()), "r") as zf:
|
member.filename = member.filename.split("/")[-1]
|
||||||
# for member in zf.infolist():
|
if member.filename.endswith("bin.x86_64"):
|
||||||
# zf.extract(member, path=target)
|
member.filename = "MiniInstaller.bin.x86_64"
|
||||||
# installer = subprocess.Popen([os.path.join(target, "precompiled"),
|
zf.extract(member, path=game_folder)
|
||||||
# os.path.join(folder, "MiniInstaller.exe")], shell=False)
|
files.append(member.filename)
|
||||||
# os.remove(target)
|
mono_installer = os.path.join(game_folder, "MiniInstaller.bin.x86_64")
|
||||||
|
os.chmod(mono_installer, 0o755)
|
||||||
|
installer = subprocess.Popen(mono_installer, shell=False)
|
||||||
|
failure = installer.wait()
|
||||||
|
for file in files:
|
||||||
|
os.remove(file)
|
||||||
else:
|
else:
|
||||||
installer = subprocess.Popen([mono_exe, os.path.join(game_folder, "MiniInstaller.exe")], shell=False)
|
installer = subprocess.Popen([mono_exe, os.path.join(game_folder, "MiniInstaller.exe")], shell=True)
|
||||||
|
failure = installer.wait()
|
||||||
else:
|
else:
|
||||||
installer = subprocess.Popen(os.path.join(game_folder, "MiniInstaller.exe"), shell=False)
|
installer = subprocess.Popen(os.path.join(game_folder, "MiniInstaller.exe"), shell=True)
|
||||||
|
failure = installer.wait()
|
||||||
|
|
||||||
failure = installer.wait()
|
print(failure)
|
||||||
if failure:
|
if failure:
|
||||||
messagebox("Failure", "Failed to install Courier", True)
|
messagebox("Failure", "Failed to install Courier", True)
|
||||||
os.chdir(working_directory)
|
os.chdir(working_directory)
|
||||||
@@ -124,18 +153,35 @@ def launch_game(url: Optional[str] = None) -> None:
|
|||||||
return "alpha" in latest_version or tuplize_version(latest_version) > tuplize_version(installed_version)
|
return "alpha" in latest_version or tuplize_version(latest_version) > tuplize_version(installed_version)
|
||||||
|
|
||||||
from . import MessengerWorld
|
from . import MessengerWorld
|
||||||
game_folder = os.path.dirname(MessengerWorld.settings.game_path)
|
try:
|
||||||
|
game_folder = os.path.dirname(MessengerWorld.settings.game_path)
|
||||||
|
except ValueError as e:
|
||||||
|
logging.error(e)
|
||||||
|
messagebox("Invalid File", "Selected file did not match expected hash. "
|
||||||
|
"Please try again and ensure you select The Messenger.exe.")
|
||||||
|
return
|
||||||
working_directory = os.getcwd()
|
working_directory = os.getcwd()
|
||||||
|
# setup ssl context
|
||||||
|
try:
|
||||||
|
import certifi
|
||||||
|
import ssl
|
||||||
|
context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH, cafile=certifi.where())
|
||||||
|
context.set_alpn_protocols(["http/1.1"])
|
||||||
|
https_handler = urllib.request.HTTPSHandler(context=context)
|
||||||
|
opener = urllib.request.build_opener(https_handler)
|
||||||
|
urllib.request.install_opener(opener)
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
if not courier_installed():
|
if not courier_installed():
|
||||||
should_install = askyesnocancel("Install Courier",
|
should_install = ask_yes_no_cancel("Install Courier",
|
||||||
"No Courier installation detected. Would you like to install now?")
|
"No Courier installation detected. Would you like to install now?")
|
||||||
if not should_install:
|
if not should_install:
|
||||||
return
|
return
|
||||||
logging.info("Installing Courier")
|
logging.info("Installing Courier")
|
||||||
install_courier()
|
install_courier()
|
||||||
if not mod_installed():
|
if not mod_installed():
|
||||||
should_install = askyesnocancel("Install Mod",
|
should_install = ask_yes_no_cancel("Install Mod",
|
||||||
"No randomizer mod detected. Would you like to install now?")
|
"No randomizer mod detected. Would you like to install now?")
|
||||||
if not should_install:
|
if not should_install:
|
||||||
return
|
return
|
||||||
logging.info("Installing Mod")
|
logging.info("Installing Mod")
|
||||||
@@ -143,22 +189,33 @@ def launch_game(url: Optional[str] = None) -> None:
|
|||||||
else:
|
else:
|
||||||
latest = request_data(MOD_URL)["tag_name"]
|
latest = request_data(MOD_URL)["tag_name"]
|
||||||
if available_mod_update(latest):
|
if available_mod_update(latest):
|
||||||
should_update = askyesnocancel("Update Mod",
|
should_update = ask_yes_no_cancel("Update Mod",
|
||||||
f"New mod version detected. Would you like to update to {latest} now?")
|
f"New mod version detected. Would you like to update to {latest} now?")
|
||||||
if should_update:
|
if should_update:
|
||||||
logging.info("Updating mod")
|
logging.info("Updating mod")
|
||||||
install_mod()
|
install_mod()
|
||||||
elif should_update is None:
|
elif should_update is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if not args:
|
||||||
|
should_launch = ask_yes_no_cancel("Launch Game",
|
||||||
|
"Mod installed and up to date. Would you like to launch the game now?")
|
||||||
|
if not should_launch:
|
||||||
|
return
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser(description="Messenger Client Launcher")
|
||||||
|
parser.add_argument("url", type=str, nargs="?", help="Archipelago Webhost uri to auto connect to.")
|
||||||
|
args = parser.parse_args(args)
|
||||||
|
|
||||||
if not is_windows:
|
if not is_windows:
|
||||||
if url:
|
if args.url:
|
||||||
open_file(f"steam://rungameid/764790//{url}/")
|
open_file(f"steam://rungameid/764790//{args.url}/")
|
||||||
else:
|
else:
|
||||||
open_file("steam://rungameid/764790")
|
open_file("steam://rungameid/764790")
|
||||||
else:
|
else:
|
||||||
os.chdir(game_folder)
|
os.chdir(game_folder)
|
||||||
if url:
|
if args.url:
|
||||||
subprocess.Popen([MessengerWorld.settings.game_path, str(url)])
|
subprocess.Popen([MessengerWorld.settings.game_path, str(args.url)])
|
||||||
else:
|
else:
|
||||||
subprocess.Popen(MessengerWorld.settings.game_path)
|
subprocess.Popen(MessengerWorld.settings.game_path)
|
||||||
os.chdir(working_directory)
|
os.chdir(working_directory)
|
||||||
|
|||||||
@@ -39,7 +39,9 @@ You can find items wherever items can be picked up in the original game. This in
|
|||||||
|
|
||||||
When you attempt to hint for items in Archipelago you can use either the name for the specific item, or the name of a
|
When you attempt to hint for items in Archipelago you can use either the name for the specific item, or the name of a
|
||||||
group of items. Hinting for a group will choose a random item from the group that you do not currently have and hint
|
group of items. Hinting for a group will choose a random item from the group that you do not currently have and hint
|
||||||
for it. The groups you can use for The Messenger are:
|
for it.
|
||||||
|
|
||||||
|
The groups you can use for The Messenger are:
|
||||||
* Notes - This covers the music notes
|
* Notes - This covers the music notes
|
||||||
* Keys - An alternative name for the music notes
|
* Keys - An alternative name for the music notes
|
||||||
* Crest - The Sun and Moon Crests
|
* Crest - The Sun and Moon Crests
|
||||||
@@ -50,26 +52,26 @@ for it. The groups you can use for The Messenger are:
|
|||||||
|
|
||||||
* The player can return to the Tower of Time HQ at any point by selecting the button from the options menu
|
* The player can return to the Tower of Time HQ at any point by selecting the button from the options menu
|
||||||
* This can cause issues if used at specific times. If used in any of these known problematic areas, immediately
|
* This can cause issues if used at specific times. If used in any of these known problematic areas, immediately
|
||||||
quit to title and reload the save. The currently known areas include:
|
quit to title and reload the save. The currently known areas include:
|
||||||
* During Boss fights
|
* During Boss fights
|
||||||
* After Courage Note collection (Corrupted Future chase)
|
* After Courage Note collection (Corrupted Future chase)
|
||||||
* After reaching ninja village a teleport option is added to the menu to reach it quickly
|
* After reaching ninja village a teleport option is added to the menu to reach it quickly
|
||||||
* Toggle Windmill Shuriken button is added to option menu once the item is received
|
* Toggle Windmill Shuriken button is added to option menu once the item is received
|
||||||
* The mod option menu will also have a hint item button, as well as a release and collect button that are all placed
|
* The mod option menu will also have a hint item button, as well as a release and collect button that are all placed
|
||||||
when the player fulfills the necessary conditions.
|
when the player fulfills the necessary conditions.
|
||||||
* After running the game with the mod, a config file (APConfig.toml) will be generated in your game folder that can be
|
* After running the game with the mod, a config file (APConfig.toml) will be generated in your game folder that can be
|
||||||
used to modify certain settings such as text size and color. This can also be used to specify a player name that can't
|
used to modify certain settings such as text size and color. This can also be used to specify a player name that can't
|
||||||
be entered in game.
|
be entered in game.
|
||||||
|
|
||||||
## Known issues
|
## Known issues
|
||||||
* Ruxxtin Coffin cutscene will sometimes not play correctly, but will still reward the item
|
* Ruxxtin Coffin cutscene will sometimes not play correctly, but will still reward the item
|
||||||
* If you receive the Magic Firefly while in Quillshroom Marsh, The De-curse Queen cutscene will not play. You can exit
|
* If you receive the Magic Firefly while in Quillshroom Marsh, The De-curse Queen cutscene will not play. You can exit
|
||||||
to Searing Crags and re-enter to get it to play correctly.
|
to Searing Crags and re-enter to get it to play correctly.
|
||||||
* Teleporting back to HQ, then returning to the same level you just left through a Portal can cause Ninja to run left
|
* Teleporting back to HQ, then returning to the same level you just left through a Portal can cause Ninja to run left
|
||||||
and enter a different portal than the one entered by the player or lead to other incorrect inputs, causing a soft lock
|
and enter a different portal than the one entered by the player or lead to other incorrect inputs, causing a soft lock
|
||||||
* Text entry menus don't accept controller input
|
* Text entry menus don't accept controller input
|
||||||
* In power seal hunt mode, the chest must be opened by entering the shop from a level. Teleporting to HQ and opening the
|
* In power seal hunt mode, the chest must be opened by entering the shop from a level. Teleporting to HQ and opening the
|
||||||
chest will not work.
|
chest will not work.
|
||||||
|
|
||||||
## What do I do if I have a problem?
|
## What do I do if I have a problem?
|
||||||
|
|
||||||
|
|||||||
@@ -41,14 +41,27 @@ These steps can also be followed to launch the game and check for mod updates af
|
|||||||
|
|
||||||
## Joining a MultiWorld Game
|
## Joining a MultiWorld Game
|
||||||
|
|
||||||
|
### Automatic Connection on archipelago.gg
|
||||||
|
|
||||||
|
1. Go to the room page of the MultiWorld you are going to join.
|
||||||
|
2. Click on your slot name on the left side.
|
||||||
|
3. Click the "The Messenger" button in the prompt.
|
||||||
|
4. Follow the remaining prompts. This process will check that you have the mod installed and will also check for updates
|
||||||
|
before launching The Messenger. If you are using the Steam version of The Messenger you may also get a prompt from
|
||||||
|
Steam asking if the game should be launched with arguments. These arguments are the URI which the mod uses to
|
||||||
|
connect.
|
||||||
|
5. Start a new save. You will already be connected in The Messenger and do not need to go through the menus.
|
||||||
|
|
||||||
|
### Manual Connection
|
||||||
|
|
||||||
1. Launch the game
|
1. Launch the game
|
||||||
2. Navigate to `Options > Archipelago Options`
|
2. Navigate to `Options > Archipelago Options`
|
||||||
3. Enter connection info using the relevant option buttons
|
3. Enter connection info using the relevant option buttons
|
||||||
* **The game is limited to alphanumerical characters, `.`, and `-`.**
|
* **The game is limited to alphanumerical characters, `.`, and `-`.**
|
||||||
* This defaults to `archipelago.gg` and does not need to be manually changed if connecting to a game hosted on the
|
* This defaults to `archipelago.gg` and does not need to be manually changed if connecting to a game hosted on the
|
||||||
website.
|
website.
|
||||||
* If using a name that cannot be entered in the in game menus, there is a config file (APConfig.toml) in the game
|
* If using a name that cannot be entered in the in game menus, there is a config file (APConfig.toml) in the game
|
||||||
directory. When using this, all connection information must be entered in the file.
|
directory. When using this, all connection information must be entered in the file.
|
||||||
4. Select the `Connect to Archipelago` button
|
4. Select the `Connect to Archipelago` button
|
||||||
5. Navigate to save file selection
|
5. Navigate to save file selection
|
||||||
6. Start a new game
|
6. Start a new game
|
||||||
|
|||||||
@@ -215,13 +215,13 @@ def shuffle_portals(world: "MessengerWorld") -> None:
|
|||||||
|
|
||||||
if "Portal" in warp:
|
if "Portal" in warp:
|
||||||
exit_string += "Portal"
|
exit_string += "Portal"
|
||||||
world.portal_mapping.append(int(f"{REGION_ORDER.index(parent)}00"))
|
world.portal_mapping.insert(PORTALS.index(in_portal), int(f"{REGION_ORDER.index(parent)}00"))
|
||||||
elif warp in SHOP_POINTS[parent]:
|
elif warp in SHOP_POINTS[parent]:
|
||||||
exit_string += f"{warp} Shop"
|
exit_string += f"{warp} Shop"
|
||||||
world.portal_mapping.append(int(f"{REGION_ORDER.index(parent)}1{SHOP_POINTS[parent].index(warp)}"))
|
world.portal_mapping.insert(PORTALS.index(in_portal), int(f"{REGION_ORDER.index(parent)}1{SHOP_POINTS[parent].index(warp)}"))
|
||||||
else:
|
else:
|
||||||
exit_string += f"{warp} Checkpoint"
|
exit_string += f"{warp} Checkpoint"
|
||||||
world.portal_mapping.append(int(f"{REGION_ORDER.index(parent)}2{CHECKPOINTS[parent].index(warp)}"))
|
world.portal_mapping.insert(PORTALS.index(in_portal), int(f"{REGION_ORDER.index(parent)}2{CHECKPOINTS[parent].index(warp)}"))
|
||||||
|
|
||||||
world.spoiler_portal_mapping[in_portal] = exit_string
|
world.spoiler_portal_mapping[in_portal] = exit_string
|
||||||
connect_portal(world, in_portal, exit_string)
|
connect_portal(world, in_portal, exit_string)
|
||||||
@@ -230,12 +230,15 @@ def shuffle_portals(world: "MessengerWorld") -> None:
|
|||||||
|
|
||||||
def handle_planned_portals(plando_connections: List[PlandoConnection]) -> None:
|
def handle_planned_portals(plando_connections: List[PlandoConnection]) -> None:
|
||||||
"""checks the provided plando connections for portals and connects them"""
|
"""checks the provided plando connections for portals and connects them"""
|
||||||
|
nonlocal available_portals
|
||||||
|
|
||||||
for connection in plando_connections:
|
for connection in plando_connections:
|
||||||
if connection.entrance not in PORTALS:
|
|
||||||
continue
|
|
||||||
# let it crash here if input is invalid
|
# let it crash here if input is invalid
|
||||||
create_mapping(connection.entrance, connection.exit)
|
available_portals.remove(connection.exit)
|
||||||
|
parent = create_mapping(connection.entrance, connection.exit)
|
||||||
world.plando_portals.append(connection.entrance)
|
world.plando_portals.append(connection.entrance)
|
||||||
|
if shuffle_type < ShufflePortals.option_anywhere:
|
||||||
|
available_portals = [port for port in available_portals if port not in shop_points[parent]]
|
||||||
|
|
||||||
shuffle_type = world.options.shuffle_portals
|
shuffle_type = world.options.shuffle_portals
|
||||||
shop_points = deepcopy(SHOP_POINTS)
|
shop_points = deepcopy(SHOP_POINTS)
|
||||||
@@ -251,8 +254,13 @@ def shuffle_portals(world: "MessengerWorld") -> None:
|
|||||||
plando = world.options.portal_plando.value
|
plando = world.options.portal_plando.value
|
||||||
if not plando:
|
if not plando:
|
||||||
plando = world.options.plando_connections.value
|
plando = world.options.plando_connections.value
|
||||||
if plando and world.multiworld.plando_options & PlandoOptions.connections:
|
if plando and world.multiworld.plando_options & PlandoOptions.connections and not world.plando_portals:
|
||||||
handle_planned_portals(plando)
|
try:
|
||||||
|
handle_planned_portals(plando)
|
||||||
|
# any failure i expect will trigger on available_portals.remove
|
||||||
|
except ValueError:
|
||||||
|
raise ValueError(f"Unable to complete portal plando for Player {world.player_name}. "
|
||||||
|
f"If you attempted to plando a checkpoint, checkpoints must be shuffled.")
|
||||||
|
|
||||||
for portal in PORTALS:
|
for portal in PORTALS:
|
||||||
if portal in world.plando_portals:
|
if portal in world.plando_portals:
|
||||||
@@ -276,8 +284,13 @@ def disconnect_portals(world: "MessengerWorld") -> None:
|
|||||||
entrance.connected_region = None
|
entrance.connected_region = None
|
||||||
if portal in world.spoiler_portal_mapping:
|
if portal in world.spoiler_portal_mapping:
|
||||||
del world.spoiler_portal_mapping[portal]
|
del world.spoiler_portal_mapping[portal]
|
||||||
if len(world.portal_mapping) > len(world.spoiler_portal_mapping):
|
if world.plando_portals:
|
||||||
world.portal_mapping = world.portal_mapping[:len(world.spoiler_portal_mapping)]
|
indexes = [PORTALS.index(portal) for portal in world.plando_portals]
|
||||||
|
planned_portals = []
|
||||||
|
for index, portal_coord in enumerate(world.portal_mapping):
|
||||||
|
if index in indexes:
|
||||||
|
planned_portals.append(portal_coord)
|
||||||
|
world.portal_mapping = planned_portals
|
||||||
|
|
||||||
|
|
||||||
def validate_portals(world: "MessengerWorld") -> bool:
|
def validate_portals(world: "MessengerWorld") -> bool:
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ class MLSSClient(BizHawkClient):
|
|||||||
if not self.seed_verify:
|
if not self.seed_verify:
|
||||||
seed = await bizhawk.read(ctx.bizhawk_ctx, [(0xDF00A0, len(ctx.seed_name), "ROM")])
|
seed = await bizhawk.read(ctx.bizhawk_ctx, [(0xDF00A0, len(ctx.seed_name), "ROM")])
|
||||||
seed = seed[0].decode("UTF-8")
|
seed = seed[0].decode("UTF-8")
|
||||||
if seed != ctx.seed_name:
|
if seed not in ctx.seed_name:
|
||||||
logger.info(
|
logger.info(
|
||||||
"ERROR: The ROM you loaded is for a different game of AP. "
|
"ERROR: The ROM you loaded is for a different game of AP. "
|
||||||
"Please make sure the host has sent you the correct patch file,"
|
"Please make sure the host has sent you the correct patch file,"
|
||||||
@@ -143,17 +143,30 @@ class MLSSClient(BizHawkClient):
|
|||||||
# If RAM address isn't 0x0 yet break out and try again later to give the rest of the items
|
# If RAM address isn't 0x0 yet break out and try again later to give the rest of the items
|
||||||
for i in range(len(ctx.items_received) - received_index):
|
for i in range(len(ctx.items_received) - received_index):
|
||||||
item_data = items_by_id[ctx.items_received[received_index + i].item]
|
item_data = items_by_id[ctx.items_received[received_index + i].item]
|
||||||
b = await bizhawk.guarded_read(ctx.bizhawk_ctx, [(0x3057, 1, "EWRAM")], [(0x3057, [0x0], "EWRAM")])
|
result = False
|
||||||
if b is None:
|
total = 0
|
||||||
|
while not result:
|
||||||
|
await asyncio.sleep(0.05)
|
||||||
|
total += 0.05
|
||||||
|
result = await bizhawk.guarded_write(
|
||||||
|
ctx.bizhawk_ctx,
|
||||||
|
[
|
||||||
|
(0x3057, [id_to_RAM(item_data.itemID)], "EWRAM")
|
||||||
|
],
|
||||||
|
[(0x3057, [0x0], "EWRAM")]
|
||||||
|
)
|
||||||
|
if result:
|
||||||
|
total = 0
|
||||||
|
if total >= 1:
|
||||||
|
break
|
||||||
|
if not result:
|
||||||
break
|
break
|
||||||
await bizhawk.write(
|
await bizhawk.write(
|
||||||
ctx.bizhawk_ctx,
|
ctx.bizhawk_ctx,
|
||||||
[
|
[
|
||||||
(0x3057, [id_to_RAM(item_data.itemID)], "EWRAM"),
|
|
||||||
(0x4808, [(received_index + i + 1) // 0x100, (received_index + i + 1) % 0x100], "EWRAM"),
|
(0x4808, [(received_index + i + 1) // 0x100, (received_index + i + 1) % 0x100], "EWRAM"),
|
||||||
],
|
]
|
||||||
)
|
)
|
||||||
await asyncio.sleep(0.1)
|
|
||||||
|
|
||||||
# Early return and location send if you are currently in a shop,
|
# Early return and location send if you are currently in a shop,
|
||||||
# since other flags aren't going to change
|
# since other flags aren't going to change
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
flying = [
|
flying = [
|
||||||
0x14,
|
0x14,
|
||||||
0x1D,
|
0x1D,
|
||||||
|
0x32,
|
||||||
|
0x33,
|
||||||
|
0x40,
|
||||||
0x4C
|
0x4C
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -23,7 +26,6 @@ enemies = [
|
|||||||
0x5032AC,
|
0x5032AC,
|
||||||
0x5032CC,
|
0x5032CC,
|
||||||
0x5032EC,
|
0x5032EC,
|
||||||
0x50330C,
|
|
||||||
0x50332C,
|
0x50332C,
|
||||||
0x50334C,
|
0x50334C,
|
||||||
0x50336C,
|
0x50336C,
|
||||||
@@ -151,7 +153,7 @@ enemies = [
|
|||||||
0x50458C,
|
0x50458C,
|
||||||
0x5045AC,
|
0x5045AC,
|
||||||
0x50468C,
|
0x50468C,
|
||||||
0x5046CC,
|
# 0x5046CC, 6 enemy formation
|
||||||
0x5046EC,
|
0x5046EC,
|
||||||
0x50470C
|
0x50470C
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -78,21 +78,21 @@ itemList: typing.List[ItemData] = [
|
|||||||
ItemData(77771060, "Beanstar Piece 3", ItemClassification.progression, 0x67),
|
ItemData(77771060, "Beanstar Piece 3", ItemClassification.progression, 0x67),
|
||||||
ItemData(77771061, "Beanstar Piece 4", ItemClassification.progression, 0x70),
|
ItemData(77771061, "Beanstar Piece 4", ItemClassification.progression, 0x70),
|
||||||
ItemData(77771062, "Spangle", ItemClassification.progression, 0x72),
|
ItemData(77771062, "Spangle", ItemClassification.progression, 0x72),
|
||||||
ItemData(77771063, "Beanlet 1", ItemClassification.filler, 0x73),
|
ItemData(77771063, "Beanlet 1", ItemClassification.useful, 0x73),
|
||||||
ItemData(77771064, "Beanlet 2", ItemClassification.filler, 0x74),
|
ItemData(77771064, "Beanlet 2", ItemClassification.useful, 0x74),
|
||||||
ItemData(77771065, "Beanlet 3", ItemClassification.filler, 0x75),
|
ItemData(77771065, "Beanlet 3", ItemClassification.useful, 0x75),
|
||||||
ItemData(77771066, "Beanlet 4", ItemClassification.filler, 0x76),
|
ItemData(77771066, "Beanlet 4", ItemClassification.useful, 0x76),
|
||||||
ItemData(77771067, "Beanlet 5", ItemClassification.filler, 0x77),
|
ItemData(77771067, "Beanlet 5", ItemClassification.useful, 0x77),
|
||||||
ItemData(77771068, "Beanstone 1", ItemClassification.filler, 0x80),
|
ItemData(77771068, "Beanstone 1", ItemClassification.useful, 0x80),
|
||||||
ItemData(77771069, "Beanstone 2", ItemClassification.filler, 0x81),
|
ItemData(77771069, "Beanstone 2", ItemClassification.useful, 0x81),
|
||||||
ItemData(77771070, "Beanstone 3", ItemClassification.filler, 0x82),
|
ItemData(77771070, "Beanstone 3", ItemClassification.useful, 0x82),
|
||||||
ItemData(77771071, "Beanstone 4", ItemClassification.filler, 0x83),
|
ItemData(77771071, "Beanstone 4", ItemClassification.useful, 0x83),
|
||||||
ItemData(77771072, "Beanstone 5", ItemClassification.filler, 0x84),
|
ItemData(77771072, "Beanstone 5", ItemClassification.useful, 0x84),
|
||||||
ItemData(77771073, "Beanstone 6", ItemClassification.filler, 0x85),
|
ItemData(77771073, "Beanstone 6", ItemClassification.useful, 0x85),
|
||||||
ItemData(77771074, "Beanstone 7", ItemClassification.filler, 0x86),
|
ItemData(77771074, "Beanstone 7", ItemClassification.useful, 0x86),
|
||||||
ItemData(77771075, "Beanstone 8", ItemClassification.filler, 0x87),
|
ItemData(77771075, "Beanstone 8", ItemClassification.useful, 0x87),
|
||||||
ItemData(77771076, "Beanstone 9", ItemClassification.filler, 0x90),
|
ItemData(77771076, "Beanstone 9", ItemClassification.useful, 0x90),
|
||||||
ItemData(77771077, "Beanstone 10", ItemClassification.filler, 0x91),
|
ItemData(77771077, "Beanstone 10", ItemClassification.useful, 0x91),
|
||||||
ItemData(77771078, "Secret Scroll 1", ItemClassification.useful, 0x92),
|
ItemData(77771078, "Secret Scroll 1", ItemClassification.useful, 0x92),
|
||||||
ItemData(77771079, "Secret Scroll 2", ItemClassification.useful, 0x93),
|
ItemData(77771079, "Secret Scroll 2", ItemClassification.useful, 0x93),
|
||||||
ItemData(77771080, "Castle Badge", ItemClassification.useful, 0x9F),
|
ItemData(77771080, "Castle Badge", ItemClassification.useful, 0x9F),
|
||||||
|
|||||||
@@ -4,9 +4,6 @@ from BaseClasses import Location
|
|||||||
|
|
||||||
|
|
||||||
class LocationData:
|
class LocationData:
|
||||||
name: str = ""
|
|
||||||
id: int = 0x00
|
|
||||||
|
|
||||||
def __init__(self, name, id_, itemType):
|
def __init__(self, name, id_, itemType):
|
||||||
self.name = name
|
self.name = name
|
||||||
self.itemType = itemType
|
self.itemType = itemType
|
||||||
@@ -93,8 +90,8 @@ mainArea: typing.List[LocationData] = [
|
|||||||
LocationData("Hoohoo Mountain Below Summit Block 1", 0x39D873, 0),
|
LocationData("Hoohoo Mountain Below Summit Block 1", 0x39D873, 0),
|
||||||
LocationData("Hoohoo Mountain Below Summit Block 2", 0x39D87B, 0),
|
LocationData("Hoohoo Mountain Below Summit Block 2", 0x39D87B, 0),
|
||||||
LocationData("Hoohoo Mountain Below Summit Block 3", 0x39D883, 0),
|
LocationData("Hoohoo Mountain Below Summit Block 3", 0x39D883, 0),
|
||||||
LocationData("Hoohoo Mountain After Hoohooros Block 1", 0x39D890, 0),
|
LocationData("Hoohoo Mountain Past Hoohooros Block 1", 0x39D890, 0),
|
||||||
LocationData("Hoohoo Mountain After Hoohooros Block 2", 0x39D8A0, 0),
|
LocationData("Hoohoo Mountain Past Hoohooros Block 2", 0x39D8A0, 0),
|
||||||
LocationData("Hoohoo Mountain Hoohooros Room Block 1", 0x39D8AD, 0),
|
LocationData("Hoohoo Mountain Hoohooros Room Block 1", 0x39D8AD, 0),
|
||||||
LocationData("Hoohoo Mountain Hoohooros Room Block 2", 0x39D8B5, 0),
|
LocationData("Hoohoo Mountain Hoohooros Room Block 2", 0x39D8B5, 0),
|
||||||
LocationData("Hoohoo Mountain Before Hoohooros Block", 0x39D8D2, 0),
|
LocationData("Hoohoo Mountain Before Hoohooros Block", 0x39D8D2, 0),
|
||||||
@@ -104,7 +101,7 @@ mainArea: typing.List[LocationData] = [
|
|||||||
LocationData("Hoohoo Mountain Room 1 Block 2", 0x39D924, 0),
|
LocationData("Hoohoo Mountain Room 1 Block 2", 0x39D924, 0),
|
||||||
LocationData("Hoohoo Mountain Room 1 Block 3", 0x39D92C, 0),
|
LocationData("Hoohoo Mountain Room 1 Block 3", 0x39D92C, 0),
|
||||||
LocationData("Hoohoo Mountain Base Room 1 Block", 0x39D939, 0),
|
LocationData("Hoohoo Mountain Base Room 1 Block", 0x39D939, 0),
|
||||||
LocationData("Hoohoo Village Right Side Block", 0x39D957, 0),
|
LocationData("Hoohoo Village Eastside Block", 0x39D957, 0),
|
||||||
LocationData("Hoohoo Village Bridge Room Block 1", 0x39D96F, 0),
|
LocationData("Hoohoo Village Bridge Room Block 1", 0x39D96F, 0),
|
||||||
LocationData("Hoohoo Village Bridge Room Block 2", 0x39D97F, 0),
|
LocationData("Hoohoo Village Bridge Room Block 2", 0x39D97F, 0),
|
||||||
LocationData("Hoohoo Village Bridge Room Block 3", 0x39D98F, 0),
|
LocationData("Hoohoo Village Bridge Room Block 3", 0x39D98F, 0),
|
||||||
@@ -119,8 +116,8 @@ mainArea: typing.List[LocationData] = [
|
|||||||
LocationData("Hoohoo Mountain Base Boostatue Room Digspot 2", 0x39D9E1, 0),
|
LocationData("Hoohoo Mountain Base Boostatue Room Digspot 2", 0x39D9E1, 0),
|
||||||
LocationData("Hoohoo Mountain Base Grassy Area Block 1", 0x39D9FE, 0),
|
LocationData("Hoohoo Mountain Base Grassy Area Block 1", 0x39D9FE, 0),
|
||||||
LocationData("Hoohoo Mountain Base Grassy Area Block 2", 0x39D9F6, 0),
|
LocationData("Hoohoo Mountain Base Grassy Area Block 2", 0x39D9F6, 0),
|
||||||
LocationData("Hoohoo Mountain Base After Minecart Minigame Block 1", 0x39DA35, 0),
|
LocationData("Hoohoo Mountain Base Past Minecart Minigame Block 1", 0x39DA35, 0),
|
||||||
LocationData("Hoohoo Mountain Base After Minecart Minigame Block 2", 0x39DA2D, 0),
|
LocationData("Hoohoo Mountain Base Past Minecart Minigame Block 2", 0x39DA2D, 0),
|
||||||
LocationData("Cave Connecting Stardust Fields and Hoohoo Village Block 1", 0x39DA77, 0),
|
LocationData("Cave Connecting Stardust Fields and Hoohoo Village Block 1", 0x39DA77, 0),
|
||||||
LocationData("Cave Connecting Stardust Fields and Hoohoo Village Block 2", 0x39DA7F, 0),
|
LocationData("Cave Connecting Stardust Fields and Hoohoo Village Block 2", 0x39DA7F, 0),
|
||||||
LocationData("Hoohoo Village South Cave Block", 0x39DACD, 0),
|
LocationData("Hoohoo Village South Cave Block", 0x39DACD, 0),
|
||||||
@@ -143,14 +140,14 @@ mainArea: typing.List[LocationData] = [
|
|||||||
LocationData("Shop Starting Flag 3", 0x3C05F4, 3),
|
LocationData("Shop Starting Flag 3", 0x3C05F4, 3),
|
||||||
LocationData("Hoohoo Mountain Summit Digspot", 0x39D85E, 0),
|
LocationData("Hoohoo Mountain Summit Digspot", 0x39D85E, 0),
|
||||||
LocationData("Hoohoo Mountain Below Summit Digspot", 0x39D86B, 0),
|
LocationData("Hoohoo Mountain Below Summit Digspot", 0x39D86B, 0),
|
||||||
LocationData("Hoohoo Mountain After Hoohooros Digspot", 0x39D898, 0),
|
LocationData("Hoohoo Mountain Past Hoohooros Digspot", 0x39D898, 0),
|
||||||
LocationData("Hoohoo Mountain Hoohooros Room Digspot 1", 0x39D8BD, 0),
|
LocationData("Hoohoo Mountain Hoohooros Room Digspot 1", 0x39D8BD, 0),
|
||||||
LocationData("Hoohoo Mountain Hoohooros Room Digspot 2", 0x39D8C5, 0),
|
LocationData("Hoohoo Mountain Hoohooros Room Digspot 2", 0x39D8C5, 0),
|
||||||
LocationData("Hoohoo Mountain Before Hoohooros Digspot", 0x39D8E2, 0),
|
LocationData("Hoohoo Mountain Before Hoohooros Digspot", 0x39D8E2, 0),
|
||||||
LocationData("Hoohoo Mountain Room 2 Digspot 1", 0x39D907, 0),
|
LocationData("Hoohoo Mountain Room 2 Digspot 1", 0x39D907, 0),
|
||||||
LocationData("Hoohoo Mountain Room 2 Digspot 2", 0x39D90F, 0),
|
LocationData("Hoohoo Mountain Room 2 Digspot 2", 0x39D90F, 0),
|
||||||
LocationData("Hoohoo Mountain Base Room 1 Digspot", 0x39D941, 0),
|
LocationData("Hoohoo Mountain Base Room 1 Digspot", 0x39D941, 0),
|
||||||
LocationData("Hoohoo Village Right Side Digspot", 0x39D95F, 0),
|
LocationData("Hoohoo Village Eastside Digspot", 0x39D95F, 0),
|
||||||
LocationData("Hoohoo Village Super Hammer Cave Digspot", 0x39DB02, 0),
|
LocationData("Hoohoo Village Super Hammer Cave Digspot", 0x39DB02, 0),
|
||||||
LocationData("Hoohoo Village Super Hammer Cave Block", 0x39DAEA, 0),
|
LocationData("Hoohoo Village Super Hammer Cave Block", 0x39DAEA, 0),
|
||||||
LocationData("Hoohoo Village North Cave Room 2 Digspot", 0x39DAB5, 0),
|
LocationData("Hoohoo Village North Cave Room 2 Digspot", 0x39DAB5, 0),
|
||||||
@@ -267,7 +264,7 @@ coins: typing.List[LocationData] = [
|
|||||||
LocationData("Chucklehuck Woods Cave Room 3 Coin Block", 0x39DDB4, 0),
|
LocationData("Chucklehuck Woods Cave Room 3 Coin Block", 0x39DDB4, 0),
|
||||||
LocationData("Chucklehuck Woods Pipe 5 Room Coin Block", 0x39DDE6, 0),
|
LocationData("Chucklehuck Woods Pipe 5 Room Coin Block", 0x39DDE6, 0),
|
||||||
LocationData("Chucklehuck Woods Room 7 Coin Block", 0x39DE31, 0),
|
LocationData("Chucklehuck Woods Room 7 Coin Block", 0x39DE31, 0),
|
||||||
LocationData("Chucklehuck Woods After Chuckleroot Coin Block", 0x39DF14, 0),
|
LocationData("Chucklehuck Woods Past Chuckleroot Coin Block", 0x39DF14, 0),
|
||||||
LocationData("Chucklehuck Woods Koopa Room Coin Block", 0x39DF53, 0),
|
LocationData("Chucklehuck Woods Koopa Room Coin Block", 0x39DF53, 0),
|
||||||
LocationData("Chucklehuck Woods Winkle Area Cave Coin Block", 0x39DF80, 0),
|
LocationData("Chucklehuck Woods Winkle Area Cave Coin Block", 0x39DF80, 0),
|
||||||
LocationData("Sewers Prison Room Coin Block", 0x39E01E, 0),
|
LocationData("Sewers Prison Room Coin Block", 0x39E01E, 0),
|
||||||
@@ -286,11 +283,12 @@ baseUltraRocks: typing.List[LocationData] = [
|
|||||||
LocationData("Hoohoo Mountain Base Past Ultra Hammer Rocks Block 1", 0x39DA42, 0),
|
LocationData("Hoohoo Mountain Base Past Ultra Hammer Rocks Block 1", 0x39DA42, 0),
|
||||||
LocationData("Hoohoo Mountain Base Past Ultra Hammer Rocks Block 2", 0x39DA4A, 0),
|
LocationData("Hoohoo Mountain Base Past Ultra Hammer Rocks Block 2", 0x39DA4A, 0),
|
||||||
LocationData("Hoohoo Mountain Base Past Ultra Hammer Rocks Block 3", 0x39DA52, 0),
|
LocationData("Hoohoo Mountain Base Past Ultra Hammer Rocks Block 3", 0x39DA52, 0),
|
||||||
LocationData("Hoohoo Mountain Base Boostatue Room Digspot 3 (Rightside)", 0x39D9E9, 0),
|
LocationData("Hoohoo Mountain Base Boostatue Room Digspot 3 (Right Side)", 0x39D9E9, 0),
|
||||||
LocationData("Hoohoo Mountain Base Mole Near Teehee Valley", 0x277A45, 1),
|
LocationData("Hoohoo Mountain Base Mole Near Teehee Valley", 0x277A45, 1),
|
||||||
LocationData("Teehee Valley Entrance To Hoohoo Mountain Digspot", 0x39E5B5, 0),
|
LocationData("Teehee Valley Entrance To Hoohoo Mountain Digspot", 0x39E5B5, 0),
|
||||||
LocationData("Teehee Valley Solo Luigi Maze Room 2 Digspot 1", 0x39E5C8, 0),
|
LocationData("Teehee Valley Upper Maze Room 1 Block", 0x39E5E0, 0),
|
||||||
LocationData("Teehee Valley Solo Luigi Maze Room 2 Digspot 2", 0x39E5D0, 0),
|
LocationData("Teehee Valley Upper Maze Room 2 Digspot 1", 0x39E5C8, 0),
|
||||||
|
LocationData("Teehee Valley Upper Maze Room 2 Digspot 2", 0x39E5D0, 0),
|
||||||
LocationData("Hoohoo Mountain Base Guffawha Ruins Entrance Digspot", 0x39DA0B, 0),
|
LocationData("Hoohoo Mountain Base Guffawha Ruins Entrance Digspot", 0x39DA0B, 0),
|
||||||
LocationData("Hoohoo Mountain Base Teehee Valley Entrance Digspot", 0x39DA20, 0),
|
LocationData("Hoohoo Mountain Base Teehee Valley Entrance Digspot", 0x39DA20, 0),
|
||||||
LocationData("Hoohoo Mountain Base Teehee Valley Entrance Block", 0x39DA18, 0),
|
LocationData("Hoohoo Mountain Base Teehee Valley Entrance Block", 0x39DA18, 0),
|
||||||
@@ -345,12 +343,12 @@ chucklehuck: typing.List[LocationData] = [
|
|||||||
LocationData("Chucklehuck Woods Southwest of Chuckleroot Block", 0x39DEC2, 0),
|
LocationData("Chucklehuck Woods Southwest of Chuckleroot Block", 0x39DEC2, 0),
|
||||||
LocationData("Chucklehuck Woods Wiggler room Digspot 1", 0x39DECF, 0),
|
LocationData("Chucklehuck Woods Wiggler room Digspot 1", 0x39DECF, 0),
|
||||||
LocationData("Chucklehuck Woods Wiggler room Digspot 2", 0x39DED7, 0),
|
LocationData("Chucklehuck Woods Wiggler room Digspot 2", 0x39DED7, 0),
|
||||||
LocationData("Chucklehuck Woods After Chuckleroot Block 1", 0x39DEE4, 0),
|
LocationData("Chucklehuck Woods Past Chuckleroot Block 1", 0x39DEE4, 0),
|
||||||
LocationData("Chucklehuck Woods After Chuckleroot Block 2", 0x39DEEC, 0),
|
LocationData("Chucklehuck Woods Past Chuckleroot Block 2", 0x39DEEC, 0),
|
||||||
LocationData("Chucklehuck Woods After Chuckleroot Block 3", 0x39DEF4, 0),
|
LocationData("Chucklehuck Woods Past Chuckleroot Block 3", 0x39DEF4, 0),
|
||||||
LocationData("Chucklehuck Woods After Chuckleroot Block 4", 0x39DEFC, 0),
|
LocationData("Chucklehuck Woods Past Chuckleroot Block 4", 0x39DEFC, 0),
|
||||||
LocationData("Chucklehuck Woods After Chuckleroot Block 5", 0x39DF04, 0),
|
LocationData("Chucklehuck Woods Past Chuckleroot Block 5", 0x39DF04, 0),
|
||||||
LocationData("Chucklehuck Woods After Chuckleroot Block 6", 0x39DF0C, 0),
|
LocationData("Chucklehuck Woods Past Chuckleroot Block 6", 0x39DF0C, 0),
|
||||||
LocationData("Chucklehuck Woods Koopa Room Block 1", 0x39DF4B, 0),
|
LocationData("Chucklehuck Woods Koopa Room Block 1", 0x39DF4B, 0),
|
||||||
LocationData("Chucklehuck Woods Koopa Room Block 2", 0x39DF5B, 0),
|
LocationData("Chucklehuck Woods Koopa Room Block 2", 0x39DF5B, 0),
|
||||||
LocationData("Chucklehuck Woods Koopa Room Digspot", 0x39DF63, 0),
|
LocationData("Chucklehuck Woods Koopa Room Digspot", 0x39DF63, 0),
|
||||||
@@ -367,14 +365,14 @@ chucklehuck: typing.List[LocationData] = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
castleTown: typing.List[LocationData] = [
|
castleTown: typing.List[LocationData] = [
|
||||||
LocationData("Beanbean Castle Town Left Side House Block 1", 0x39D7A4, 0),
|
LocationData("Beanbean Castle Town West Side House Block 1", 0x39D7A4, 0),
|
||||||
LocationData("Beanbean Castle Town Left Side House Block 2", 0x39D7AC, 0),
|
LocationData("Beanbean Castle Town West Side House Block 2", 0x39D7AC, 0),
|
||||||
LocationData("Beanbean Castle Town Left Side House Block 3", 0x39D7B4, 0),
|
LocationData("Beanbean Castle Town West Side House Block 3", 0x39D7B4, 0),
|
||||||
LocationData("Beanbean Castle Town Left Side House Block 4", 0x39D7BC, 0),
|
LocationData("Beanbean Castle Town West Side House Block 4", 0x39D7BC, 0),
|
||||||
LocationData("Beanbean Castle Town Right Side House Block 1", 0x39D7D8, 0),
|
LocationData("Beanbean Castle Town East Side House Block 1", 0x39D7D8, 0),
|
||||||
LocationData("Beanbean Castle Town Right Side House Block 2", 0x39D7E0, 0),
|
LocationData("Beanbean Castle Town East Side House Block 2", 0x39D7E0, 0),
|
||||||
LocationData("Beanbean Castle Town Right Side House Block 3", 0x39D7E8, 0),
|
LocationData("Beanbean Castle Town East Side House Block 3", 0x39D7E8, 0),
|
||||||
LocationData("Beanbean Castle Town Right Side House Block 4", 0x39D7F0, 0),
|
LocationData("Beanbean Castle Town East Side House Block 4", 0x39D7F0, 0),
|
||||||
LocationData("Beanbean Castle Peach's Extra Dress", 0x1E9433, 2),
|
LocationData("Beanbean Castle Peach's Extra Dress", 0x1E9433, 2),
|
||||||
LocationData("Beanbean Castle Fake Beanstar", 0x1E9432, 2),
|
LocationData("Beanbean Castle Fake Beanstar", 0x1E9432, 2),
|
||||||
LocationData("Beanbean Castle Town Beanlet 1", 0x251347, 1),
|
LocationData("Beanbean Castle Town Beanlet 1", 0x251347, 1),
|
||||||
@@ -444,14 +442,14 @@ piranhaFlag: typing.List[LocationData] = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
kidnappedFlag: typing.List[LocationData] = [
|
kidnappedFlag: typing.List[LocationData] = [
|
||||||
LocationData("Badge Shop Enter Fungitown Flag 1", 0x3C0640, 2),
|
LocationData("Badge Shop Trunkle Flag 1", 0x3C0640, 2),
|
||||||
LocationData("Badge Shop Enter Fungitown Flag 2", 0x3C0642, 2),
|
LocationData("Badge Shop Trunkle Flag 2", 0x3C0642, 2),
|
||||||
LocationData("Badge Shop Enter Fungitown Flag 3", 0x3C0644, 2),
|
LocationData("Badge Shop Trunkle Flag 3", 0x3C0644, 2),
|
||||||
LocationData("Pants Shop Enter Fungitown Flag 1", 0x3C0646, 2),
|
LocationData("Pants Shop Trunkle Flag 1", 0x3C0646, 2),
|
||||||
LocationData("Pants Shop Enter Fungitown Flag 2", 0x3C0648, 2),
|
LocationData("Pants Shop Trunkle Flag 2", 0x3C0648, 2),
|
||||||
LocationData("Pants Shop Enter Fungitown Flag 3", 0x3C064A, 2),
|
LocationData("Pants Shop Trunkle Flag 3", 0x3C064A, 2),
|
||||||
LocationData("Shop Enter Fungitown Flag 1", 0x3C0606, 3),
|
LocationData("Shop Trunkle Flag 1", 0x3C0606, 3),
|
||||||
LocationData("Shop Enter Fungitown Flag 2", 0x3C0608, 3),
|
LocationData("Shop Trunkle Flag 2", 0x3C0608, 3),
|
||||||
]
|
]
|
||||||
|
|
||||||
beanstarFlag: typing.List[LocationData] = [
|
beanstarFlag: typing.List[LocationData] = [
|
||||||
@@ -553,21 +551,21 @@ surfable: typing.List[LocationData] = [
|
|||||||
airport: typing.List[LocationData] = [
|
airport: typing.List[LocationData] = [
|
||||||
LocationData("Airport Entrance Digspot", 0x39E2DC, 0),
|
LocationData("Airport Entrance Digspot", 0x39E2DC, 0),
|
||||||
LocationData("Airport Lobby Digspot", 0x39E2E9, 0),
|
LocationData("Airport Lobby Digspot", 0x39E2E9, 0),
|
||||||
LocationData("Airport Leftside Digspot 1", 0x39E2F6, 0),
|
LocationData("Airport Westside Digspot 1", 0x39E2F6, 0),
|
||||||
LocationData("Airport Leftside Digspot 2", 0x39E2FE, 0),
|
LocationData("Airport Westside Digspot 2", 0x39E2FE, 0),
|
||||||
LocationData("Airport Leftside Digspot 3", 0x39E306, 0),
|
LocationData("Airport Westside Digspot 3", 0x39E306, 0),
|
||||||
LocationData("Airport Leftside Digspot 4", 0x39E30E, 0),
|
LocationData("Airport Westside Digspot 4", 0x39E30E, 0),
|
||||||
LocationData("Airport Leftside Digspot 5", 0x39E316, 0),
|
LocationData("Airport Westside Digspot 5", 0x39E316, 0),
|
||||||
LocationData("Airport Center Digspot 1", 0x39E323, 0),
|
LocationData("Airport Center Digspot 1", 0x39E323, 0),
|
||||||
LocationData("Airport Center Digspot 2", 0x39E32B, 0),
|
LocationData("Airport Center Digspot 2", 0x39E32B, 0),
|
||||||
LocationData("Airport Center Digspot 3", 0x39E333, 0),
|
LocationData("Airport Center Digspot 3", 0x39E333, 0),
|
||||||
LocationData("Airport Center Digspot 4", 0x39E33B, 0),
|
LocationData("Airport Center Digspot 4", 0x39E33B, 0),
|
||||||
LocationData("Airport Center Digspot 5", 0x39E343, 0),
|
LocationData("Airport Center Digspot 5", 0x39E343, 0),
|
||||||
LocationData("Airport Rightside Digspot 1", 0x39E350, 0),
|
LocationData("Airport Eastside Digspot 1", 0x39E350, 0),
|
||||||
LocationData("Airport Rightside Digspot 2", 0x39E358, 0),
|
LocationData("Airport Eastside Digspot 2", 0x39E358, 0),
|
||||||
LocationData("Airport Rightside Digspot 3", 0x39E360, 0),
|
LocationData("Airport Eastside Digspot 3", 0x39E360, 0),
|
||||||
LocationData("Airport Rightside Digspot 4", 0x39E368, 0),
|
LocationData("Airport Eastside Digspot 4", 0x39E368, 0),
|
||||||
LocationData("Airport Rightside Digspot 5", 0x39E370, 0),
|
LocationData("Airport Eastside Digspot 5", 0x39E370, 0),
|
||||||
]
|
]
|
||||||
|
|
||||||
gwarharEntrance: typing.List[LocationData] = [
|
gwarharEntrance: typing.List[LocationData] = [
|
||||||
@@ -617,7 +615,6 @@ teeheeValley: typing.List[LocationData] = [
|
|||||||
LocationData("Teehee Valley Past Ultra Hammer Rock Block 2", 0x39E590, 0),
|
LocationData("Teehee Valley Past Ultra Hammer Rock Block 2", 0x39E590, 0),
|
||||||
LocationData("Teehee Valley Past Ultra Hammer Rock Digspot 1", 0x39E598, 0),
|
LocationData("Teehee Valley Past Ultra Hammer Rock Digspot 1", 0x39E598, 0),
|
||||||
LocationData("Teehee Valley Past Ultra Hammer Rock Digspot 3", 0x39E5A8, 0),
|
LocationData("Teehee Valley Past Ultra Hammer Rock Digspot 3", 0x39E5A8, 0),
|
||||||
LocationData("Teehee Valley Solo Luigi Maze Room 1 Block", 0x39E5E0, 0),
|
|
||||||
LocationData("Teehee Valley Before Trunkle Digspot", 0x39E5F0, 0),
|
LocationData("Teehee Valley Before Trunkle Digspot", 0x39E5F0, 0),
|
||||||
LocationData("S.S. Chuckola Storage Room Block 1", 0x39E610, 0),
|
LocationData("S.S. Chuckola Storage Room Block 1", 0x39E610, 0),
|
||||||
LocationData("S.S. Chuckola Storage Room Block 2", 0x39E628, 0),
|
LocationData("S.S. Chuckola Storage Room Block 2", 0x39E628, 0),
|
||||||
@@ -667,7 +664,7 @@ bowsers: typing.List[LocationData] = [
|
|||||||
LocationData("Bowser's Castle Iggy & Morton Hallway Block 1", 0x39E9EF, 0),
|
LocationData("Bowser's Castle Iggy & Morton Hallway Block 1", 0x39E9EF, 0),
|
||||||
LocationData("Bowser's Castle Iggy & Morton Hallway Block 2", 0x39E9F7, 0),
|
LocationData("Bowser's Castle Iggy & Morton Hallway Block 2", 0x39E9F7, 0),
|
||||||
LocationData("Bowser's Castle Iggy & Morton Hallway Digspot", 0x39E9FF, 0),
|
LocationData("Bowser's Castle Iggy & Morton Hallway Digspot", 0x39E9FF, 0),
|
||||||
LocationData("Bowser's Castle After Morton Block", 0x39EA0C, 0),
|
LocationData("Bowser's Castle Past Morton Block", 0x39EA0C, 0),
|
||||||
LocationData("Bowser's Castle Morton Room 1 Digspot", 0x39EA89, 0),
|
LocationData("Bowser's Castle Morton Room 1 Digspot", 0x39EA89, 0),
|
||||||
LocationData("Bowser's Castle Lemmy Room 1 Block", 0x39EA9C, 0),
|
LocationData("Bowser's Castle Lemmy Room 1 Block", 0x39EA9C, 0),
|
||||||
LocationData("Bowser's Castle Lemmy Room 1 Digspot", 0x39EAA4, 0),
|
LocationData("Bowser's Castle Lemmy Room 1 Digspot", 0x39EAA4, 0),
|
||||||
@@ -705,16 +702,16 @@ jokesEntrance: typing.List[LocationData] = [
|
|||||||
LocationData("Joke's End Second Floor West Room Block 4", 0x39E781, 0),
|
LocationData("Joke's End Second Floor West Room Block 4", 0x39E781, 0),
|
||||||
LocationData("Joke's End Mole Reward 1", 0x27788E, 1),
|
LocationData("Joke's End Mole Reward 1", 0x27788E, 1),
|
||||||
LocationData("Joke's End Mole Reward 2", 0x2778D2, 1),
|
LocationData("Joke's End Mole Reward 2", 0x2778D2, 1),
|
||||||
]
|
|
||||||
|
|
||||||
jokesMain: typing.List[LocationData] = [
|
|
||||||
LocationData("Joke's End Furnace Room 1 Block 1", 0x39E70F, 0),
|
LocationData("Joke's End Furnace Room 1 Block 1", 0x39E70F, 0),
|
||||||
LocationData("Joke's End Furnace Room 1 Block 2", 0x39E717, 0),
|
LocationData("Joke's End Furnace Room 1 Block 2", 0x39E717, 0),
|
||||||
LocationData("Joke's End Furnace Room 1 Block 3", 0x39E71F, 0),
|
LocationData("Joke's End Furnace Room 1 Block 3", 0x39E71F, 0),
|
||||||
LocationData("Joke's End Northeast of Boiler Room 1 Block", 0x39E732, 0),
|
LocationData("Joke's End Northeast of Boiler Room 1 Block", 0x39E732, 0),
|
||||||
LocationData("Joke's End Northeast of Boiler Room 3 Digspot", 0x39E73F, 0),
|
|
||||||
LocationData("Joke's End Northeast of Boiler Room 2 Block", 0x39E74C, 0),
|
LocationData("Joke's End Northeast of Boiler Room 2 Block", 0x39E74C, 0),
|
||||||
LocationData("Joke's End Northeast of Boiler Room 2 Digspot", 0x39E754, 0),
|
LocationData("Joke's End Northeast of Boiler Room 2 Digspot", 0x39E754, 0),
|
||||||
|
LocationData("Joke's End Northeast of Boiler Room 3 Digspot", 0x39E73F, 0),
|
||||||
|
]
|
||||||
|
|
||||||
|
jokesMain: typing.List[LocationData] = [
|
||||||
LocationData("Joke's End Second Floor East Room Digspot", 0x39E794, 0),
|
LocationData("Joke's End Second Floor East Room Digspot", 0x39E794, 0),
|
||||||
LocationData("Joke's End Final Split up Room Digspot", 0x39E7A7, 0),
|
LocationData("Joke's End Final Split up Room Digspot", 0x39E7A7, 0),
|
||||||
LocationData("Joke's End South of Bridge Room Block", 0x39E7B4, 0),
|
LocationData("Joke's End South of Bridge Room Block", 0x39E7B4, 0),
|
||||||
@@ -740,10 +737,10 @@ jokesMain: typing.List[LocationData] = [
|
|||||||
|
|
||||||
postJokes: typing.List[LocationData] = [
|
postJokes: typing.List[LocationData] = [
|
||||||
LocationData("Teehee Valley Past Ultra Hammer Rock Digspot 2 (Post-Birdo)", 0x39E5A0, 0),
|
LocationData("Teehee Valley Past Ultra Hammer Rock Digspot 2 (Post-Birdo)", 0x39E5A0, 0),
|
||||||
LocationData("Teehee Valley Before Popple Digspot 1", 0x39E55B, 0),
|
LocationData("Teehee Valley Before Birdo Digspot 1", 0x39E55B, 0),
|
||||||
LocationData("Teehee Valley Before Popple Digspot 2", 0x39E563, 0),
|
LocationData("Teehee Valley Before Birdo Digspot 2", 0x39E563, 0),
|
||||||
LocationData("Teehee Valley Before Popple Digspot 3", 0x39E56B, 0),
|
LocationData("Teehee Valley Before Birdo Digspot 3", 0x39E56B, 0),
|
||||||
LocationData("Teehee Valley Before Popple Digspot 4", 0x39E573, 0),
|
LocationData("Teehee Valley Before Birdo Digspot 4", 0x39E573, 0),
|
||||||
]
|
]
|
||||||
|
|
||||||
theater: typing.List[LocationData] = [
|
theater: typing.List[LocationData] = [
|
||||||
@@ -766,6 +763,10 @@ oasis: typing.List[LocationData] = [
|
|||||||
LocationData("Oho Oasis Thunderhand", 0x1E9409, 2),
|
LocationData("Oho Oasis Thunderhand", 0x1E9409, 2),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
cacklettas_soul: typing.List[LocationData] = [
|
||||||
|
LocationData("Cackletta's Soul", None, 0),
|
||||||
|
]
|
||||||
|
|
||||||
nonBlock = [
|
nonBlock = [
|
||||||
(0x434B, 0x1, 0x243844), # Farm Mole 1
|
(0x434B, 0x1, 0x243844), # Farm Mole 1
|
||||||
(0x434B, 0x1, 0x24387D), # Farm Mole 2
|
(0x434B, 0x1, 0x24387D), # Farm Mole 2
|
||||||
@@ -1171,15 +1172,15 @@ all_locations: typing.List[LocationData] = (
|
|||||||
+ fungitownBeanstar
|
+ fungitownBeanstar
|
||||||
+ fungitownBirdo
|
+ fungitownBirdo
|
||||||
+ bowsers
|
+ bowsers
|
||||||
|
+ bowsersMini
|
||||||
+ jokesEntrance
|
+ jokesEntrance
|
||||||
+ jokesMain
|
+ jokesMain
|
||||||
+ postJokes
|
+ postJokes
|
||||||
+ theater
|
+ theater
|
||||||
+ oasis
|
+ oasis
|
||||||
+ gwarharMain
|
+ gwarharMain
|
||||||
+ bowsersMini
|
|
||||||
+ baseUltraRocks
|
+ baseUltraRocks
|
||||||
+ coins
|
+ coins
|
||||||
)
|
)
|
||||||
|
|
||||||
location_table: typing.Dict[str, int] = {locData.name: locData.id for locData in all_locations}
|
location_table: typing.Dict[str, int] = {location.name: location.id for location in all_locations}
|
||||||
|
|||||||
@@ -8,14 +8,14 @@ class LocationName:
|
|||||||
StardustFields4Block3 = "Stardust Fields Room 4 Block 3"
|
StardustFields4Block3 = "Stardust Fields Room 4 Block 3"
|
||||||
StardustFields5Block = "Stardust Fields Room 5 Block"
|
StardustFields5Block = "Stardust Fields Room 5 Block"
|
||||||
HoohooVillageHammerHouseBlock = "Hoohoo Village Hammer House Block"
|
HoohooVillageHammerHouseBlock = "Hoohoo Village Hammer House Block"
|
||||||
BeanbeanCastleTownLeftSideHouseBlock1 = "Beanbean Castle Town Left Side House Block 1"
|
BeanbeanCastleTownWestsideHouseBlock1 = "Beanbean Castle Town Westside House Block 1"
|
||||||
BeanbeanCastleTownLeftSideHouseBlock2 = "Beanbean Castle Town Left Side House Block 2"
|
BeanbeanCastleTownWestsideHouseBlock2 = "Beanbean Castle Town Westside House Block 2"
|
||||||
BeanbeanCastleTownLeftSideHouseBlock3 = "Beanbean Castle Town Left Side House Block 3"
|
BeanbeanCastleTownWestsideHouseBlock3 = "Beanbean Castle Town Westside House Block 3"
|
||||||
BeanbeanCastleTownLeftSideHouseBlock4 = "Beanbean Castle Town Left Side House Block 4"
|
BeanbeanCastleTownWestsideHouseBlock4 = "Beanbean Castle Town Westside House Block 4"
|
||||||
BeanbeanCastleTownRightSideHouseBlock1 = "Beanbean Castle Town Right Side House Block 1"
|
BeanbeanCastleTownEastsideHouseBlock1 = "Beanbean Castle Town Eastside House Block 1"
|
||||||
BeanbeanCastleTownRightSideHouseBlock2 = "Beanbean Castle Town Right Side House Block 2"
|
BeanbeanCastleTownEastsideHouseBlock2 = "Beanbean Castle Town Eastside House Block 2"
|
||||||
BeanbeanCastleTownRightSideHouseBlock3 = "Beanbean Castle Town Right Side House Block 3"
|
BeanbeanCastleTownEastsideHouseBlock3 = "Beanbean Castle Town Eastside House Block 3"
|
||||||
BeanbeanCastleTownRightSideHouseBlock4 = "Beanbean Castle Town Right Side House Block 4"
|
BeanbeanCastleTownEastsideHouseBlock4 = "Beanbean Castle Town Eastside House Block 4"
|
||||||
BeanbeanCastleTownMiniMarioBlock1 = "Beanbean Castle Town Mini Mario Block 1"
|
BeanbeanCastleTownMiniMarioBlock1 = "Beanbean Castle Town Mini Mario Block 1"
|
||||||
BeanbeanCastleTownMiniMarioBlock2 = "Beanbean Castle Town Mini Mario Block 2"
|
BeanbeanCastleTownMiniMarioBlock2 = "Beanbean Castle Town Mini Mario Block 2"
|
||||||
BeanbeanCastleTownMiniMarioBlock3 = "Beanbean Castle Town Mini Mario Block 3"
|
BeanbeanCastleTownMiniMarioBlock3 = "Beanbean Castle Town Mini Mario Block 3"
|
||||||
@@ -26,9 +26,9 @@ class LocationName:
|
|||||||
HoohooMountainBelowSummitBlock1 = "Hoohoo Mountain Below Summit Block 1"
|
HoohooMountainBelowSummitBlock1 = "Hoohoo Mountain Below Summit Block 1"
|
||||||
HoohooMountainBelowSummitBlock2 = "Hoohoo Mountain Below Summit Block 2"
|
HoohooMountainBelowSummitBlock2 = "Hoohoo Mountain Below Summit Block 2"
|
||||||
HoohooMountainBelowSummitBlock3 = "Hoohoo Mountain Below Summit Block 3"
|
HoohooMountainBelowSummitBlock3 = "Hoohoo Mountain Below Summit Block 3"
|
||||||
HoohooMountainAfterHoohoorosBlock1 = "Hoohoo Mountain After Hoohooros Block 1"
|
HoohooMountainPastHoohoorosBlock1 = "Hoohoo Mountain Past Hoohooros Block 1"
|
||||||
HoohooMountainAfterHoohoorosDigspot = "Hoohoo Mountain After Hoohooros Digspot"
|
HoohooMountainPastHoohoorosDigspot = "Hoohoo Mountain Past Hoohooros Digspot"
|
||||||
HoohooMountainAfterHoohoorosBlock2 = "Hoohoo Mountain After Hoohooros Block 2"
|
HoohooMountainPastHoohoorosBlock2 = "Hoohoo Mountain Past Hoohooros Block 2"
|
||||||
HoohooMountainHoohoorosRoomBlock1 = "Hoohoo Mountain Hoohooros Room Block 1"
|
HoohooMountainHoohoorosRoomBlock1 = "Hoohoo Mountain Hoohooros Room Block 1"
|
||||||
HoohooMountainHoohoorosRoomBlock2 = "Hoohoo Mountain Hoohooros Room Block 2"
|
HoohooMountainHoohoorosRoomBlock2 = "Hoohoo Mountain Hoohooros Room Block 2"
|
||||||
HoohooMountainHoohoorosRoomDigspot1 = "Hoohoo Mountain Hoohooros Room Digspot 1"
|
HoohooMountainHoohoorosRoomDigspot1 = "Hoohoo Mountain Hoohooros Room Digspot 1"
|
||||||
@@ -44,8 +44,8 @@ class LocationName:
|
|||||||
HoohooMountainRoom1Block3 = "Hoohoo Mountain Room 1 Block 3"
|
HoohooMountainRoom1Block3 = "Hoohoo Mountain Room 1 Block 3"
|
||||||
HoohooMountainBaseRoom1Block = "Hoohoo Mountain Base Room 1 Block"
|
HoohooMountainBaseRoom1Block = "Hoohoo Mountain Base Room 1 Block"
|
||||||
HoohooMountainBaseRoom1Digspot = "Hoohoo Mountain Base Room 1 Digspot"
|
HoohooMountainBaseRoom1Digspot = "Hoohoo Mountain Base Room 1 Digspot"
|
||||||
HoohooVillageRightSideBlock = "Hoohoo Village Right Side Block"
|
HoohooVillageEastsideBlock = "Hoohoo Village Eastside Block"
|
||||||
HoohooVillageRightSideDigspot = "Hoohoo Village Right Side Digspot"
|
HoohooVillageEastsideDigspot = "Hoohoo Village Eastside Digspot"
|
||||||
HoohooVillageBridgeRoomBlock1 = "Hoohoo Village Bridge Room Block 1"
|
HoohooVillageBridgeRoomBlock1 = "Hoohoo Village Bridge Room Block 1"
|
||||||
HoohooVillageBridgeRoomBlock2 = "Hoohoo Village Bridge Room Block 2"
|
HoohooVillageBridgeRoomBlock2 = "Hoohoo Village Bridge Room Block 2"
|
||||||
HoohooVillageBridgeRoomBlock3 = "Hoohoo Village Bridge Room Block 3"
|
HoohooVillageBridgeRoomBlock3 = "Hoohoo Village Bridge Room Block 3"
|
||||||
@@ -65,8 +65,8 @@ class LocationName:
|
|||||||
HoohooMountainBaseGuffawhaRuinsEntranceDigspot = "Hoohoo Mountain Base Guffawha Ruins Entrance Digspot"
|
HoohooMountainBaseGuffawhaRuinsEntranceDigspot = "Hoohoo Mountain Base Guffawha Ruins Entrance Digspot"
|
||||||
HoohooMountainBaseTeeheeValleyEntranceDigspot = "Hoohoo Mountain Base Teehee Valley Entrance Digspot"
|
HoohooMountainBaseTeeheeValleyEntranceDigspot = "Hoohoo Mountain Base Teehee Valley Entrance Digspot"
|
||||||
HoohooMountainBaseTeeheeValleyEntranceBlock = "Hoohoo Mountain Base Teehee Valley Entrance Block"
|
HoohooMountainBaseTeeheeValleyEntranceBlock = "Hoohoo Mountain Base Teehee Valley Entrance Block"
|
||||||
HoohooMountainBaseAfterMinecartMinigameBlock1 = "Hoohoo Mountain Base After Minecart Minigame Block 1"
|
HoohooMountainBasePastMinecartMinigameBlock1 = "Hoohoo Mountain Base Past Minecart Minigame Block 1"
|
||||||
HoohooMountainBaseAfterMinecartMinigameBlock2 = "Hoohoo Mountain Base After Minecart Minigame Block 2"
|
HoohooMountainBasePastMinecartMinigameBlock2 = "Hoohoo Mountain Base Past Minecart Minigame Block 2"
|
||||||
HoohooMountainBasePastUltraHammerRocksBlock1 = "Hoohoo Mountain Base Past Ultra Hammer Rocks Block 1"
|
HoohooMountainBasePastUltraHammerRocksBlock1 = "Hoohoo Mountain Base Past Ultra Hammer Rocks Block 1"
|
||||||
HoohooMountainBasePastUltraHammerRocksBlock2 = "Hoohoo Mountain Base Past Ultra Hammer Rocks Block 2"
|
HoohooMountainBasePastUltraHammerRocksBlock2 = "Hoohoo Mountain Base Past Ultra Hammer Rocks Block 2"
|
||||||
HoohooMountainBasePastUltraHammerRocksBlock3 = "Hoohoo Mountain Base Past Ultra Hammer Rocks Block 3"
|
HoohooMountainBasePastUltraHammerRocksBlock3 = "Hoohoo Mountain Base Past Ultra Hammer Rocks Block 3"
|
||||||
@@ -148,12 +148,12 @@ class LocationName:
|
|||||||
ChucklehuckWoodsSouthwestOfChucklerootBlock = "Chucklehuck Woods Southwest of Chuckleroot Block"
|
ChucklehuckWoodsSouthwestOfChucklerootBlock = "Chucklehuck Woods Southwest of Chuckleroot Block"
|
||||||
ChucklehuckWoodsWigglerRoomDigspot1 = "Chucklehuck Woods Wiggler Room Digspot 1"
|
ChucklehuckWoodsWigglerRoomDigspot1 = "Chucklehuck Woods Wiggler Room Digspot 1"
|
||||||
ChucklehuckWoodsWigglerRoomDigspot2 = "Chucklehuck Woods Wiggler Room Digspot 2"
|
ChucklehuckWoodsWigglerRoomDigspot2 = "Chucklehuck Woods Wiggler Room Digspot 2"
|
||||||
ChucklehuckWoodsAfterChucklerootBlock1 = "Chucklehuck Woods After Chuckleroot Block 1"
|
ChucklehuckWoodsPastChucklerootBlock1 = "Chucklehuck Woods Past Chuckleroot Block 1"
|
||||||
ChucklehuckWoodsAfterChucklerootBlock2 = "Chucklehuck Woods After Chuckleroot Block 2"
|
ChucklehuckWoodsPastChucklerootBlock2 = "Chucklehuck Woods Past Chuckleroot Block 2"
|
||||||
ChucklehuckWoodsAfterChucklerootBlock3 = "Chucklehuck Woods After Chuckleroot Block 3"
|
ChucklehuckWoodsPastChucklerootBlock3 = "Chucklehuck Woods Past Chuckleroot Block 3"
|
||||||
ChucklehuckWoodsAfterChucklerootBlock4 = "Chucklehuck Woods After Chuckleroot Block 4"
|
ChucklehuckWoodsPastChucklerootBlock4 = "Chucklehuck Woods Past Chuckleroot Block 4"
|
||||||
ChucklehuckWoodsAfterChucklerootBlock5 = "Chucklehuck Woods After Chuckleroot Block 5"
|
ChucklehuckWoodsPastChucklerootBlock5 = "Chucklehuck Woods Past Chuckleroot Block 5"
|
||||||
ChucklehuckWoodsAfterChucklerootBlock6 = "Chucklehuck Woods After Chuckleroot Block 6"
|
ChucklehuckWoodsPastChucklerootBlock6 = "Chucklehuck Woods Past Chuckleroot Block 6"
|
||||||
WinkleAreaBeanstarRoomBlock = "Winkle Area Beanstar Room Block"
|
WinkleAreaBeanstarRoomBlock = "Winkle Area Beanstar Room Block"
|
||||||
WinkleAreaDigspot = "Winkle Area Digspot"
|
WinkleAreaDigspot = "Winkle Area Digspot"
|
||||||
WinkleAreaOutsideColosseumBlock = "Winkle Area Outside Colosseum Block"
|
WinkleAreaOutsideColosseumBlock = "Winkle Area Outside Colosseum Block"
|
||||||
@@ -232,21 +232,21 @@ class LocationName:
|
|||||||
WoohooHooniversityPastCacklettaRoom2Digspot = "Woohoo Hooniversity Past Cackletta Room 2 Digspot"
|
WoohooHooniversityPastCacklettaRoom2Digspot = "Woohoo Hooniversity Past Cackletta Room 2 Digspot"
|
||||||
AirportEntranceDigspot = "Airport Entrance Digspot"
|
AirportEntranceDigspot = "Airport Entrance Digspot"
|
||||||
AirportLobbyDigspot = "Airport Lobby Digspot"
|
AirportLobbyDigspot = "Airport Lobby Digspot"
|
||||||
AirportLeftsideDigspot1 = "Airport Leftside Digspot 1"
|
AirportWestsideDigspot1 = "Airport Westside Digspot 1"
|
||||||
AirportLeftsideDigspot2 = "Airport Leftside Digspot 2"
|
AirportWestsideDigspot2 = "Airport Westside Digspot 2"
|
||||||
AirportLeftsideDigspot3 = "Airport Leftside Digspot 3"
|
AirportWestsideDigspot3 = "Airport Westside Digspot 3"
|
||||||
AirportLeftsideDigspot4 = "Airport Leftside Digspot 4"
|
AirportWestsideDigspot4 = "Airport Westside Digspot 4"
|
||||||
AirportLeftsideDigspot5 = "Airport Leftside Digspot 5"
|
AirportWestsideDigspot5 = "Airport Westside Digspot 5"
|
||||||
AirportCenterDigspot1 = "Airport Center Digspot 1"
|
AirportCenterDigspot1 = "Airport Center Digspot 1"
|
||||||
AirportCenterDigspot2 = "Airport Center Digspot 2"
|
AirportCenterDigspot2 = "Airport Center Digspot 2"
|
||||||
AirportCenterDigspot3 = "Airport Center Digspot 3"
|
AirportCenterDigspot3 = "Airport Center Digspot 3"
|
||||||
AirportCenterDigspot4 = "Airport Center Digspot 4"
|
AirportCenterDigspot4 = "Airport Center Digspot 4"
|
||||||
AirportCenterDigspot5 = "Airport Center Digspot 5"
|
AirportCenterDigspot5 = "Airport Center Digspot 5"
|
||||||
AirportRightsideDigspot1 = "Airport Rightside Digspot 1"
|
AirportEastsideDigspot1 = "Airport Eastside Digspot 1"
|
||||||
AirportRightsideDigspot2 = "Airport Rightside Digspot 2"
|
AirportEastsideDigspot2 = "Airport Eastside Digspot 2"
|
||||||
AirportRightsideDigspot3 = "Airport Rightside Digspot 3"
|
AirportEastsideDigspot3 = "Airport Eastside Digspot 3"
|
||||||
AirportRightsideDigspot4 = "Airport Rightside Digspot 4"
|
AirportEastsideDigspot4 = "Airport Eastside Digspot 4"
|
||||||
AirportRightsideDigspot5 = "Airport Rightside Digspot 5"
|
AirportEastsideDigspot5 = "Airport Eastside Digspot 5"
|
||||||
GwarharLagoonPipeRoomDigspot = "Gwarhar Lagoon Pipe Room Digspot"
|
GwarharLagoonPipeRoomDigspot = "Gwarhar Lagoon Pipe Room Digspot"
|
||||||
GwarharLagoonMassageParlorEntranceDigspot = "Gwarhar Lagoon Massage Parlor Entrance Digspot"
|
GwarharLagoonMassageParlorEntranceDigspot = "Gwarhar Lagoon Massage Parlor Entrance Digspot"
|
||||||
GwarharLagoonPastHermieDigspot = "Gwarhar Lagoon Past Hermie Digspot"
|
GwarharLagoonPastHermieDigspot = "Gwarhar Lagoon Past Hermie Digspot"
|
||||||
@@ -276,10 +276,10 @@ class LocationName:
|
|||||||
WoohooHooniversityBasementRoom4Block = "Woohoo Hooniversity Basement Room 4 Block"
|
WoohooHooniversityBasementRoom4Block = "Woohoo Hooniversity Basement Room 4 Block"
|
||||||
WoohooHooniversityPoppleRoomDigspot1 = "Woohoo Hooniversity Popple Room Digspot 1"
|
WoohooHooniversityPoppleRoomDigspot1 = "Woohoo Hooniversity Popple Room Digspot 1"
|
||||||
WoohooHooniversityPoppleRoomDigspot2 = "Woohoo Hooniversity Popple Room Digspot 2"
|
WoohooHooniversityPoppleRoomDigspot2 = "Woohoo Hooniversity Popple Room Digspot 2"
|
||||||
TeeheeValleyBeforePoppleDigspot1 = "Teehee Valley Before Popple Digspot 1"
|
TeeheeValleyBeforeBirdoDigspot1 = "Teehee Valley Before Birdo Digspot 1"
|
||||||
TeeheeValleyBeforePoppleDigspot2 = "Teehee Valley Before Popple Digspot 2"
|
TeeheeValleyBeforeBirdoDigspot2 = "Teehee Valley Before Birdo Digspot 2"
|
||||||
TeeheeValleyBeforePoppleDigspot3 = "Teehee Valley Before Popple Digspot 3"
|
TeeheeValleyBeforeBirdoDigspot3 = "Teehee Valley Before Birdo Digspot 3"
|
||||||
TeeheeValleyBeforePoppleDigspot4 = "Teehee Valley Before Popple Digspot 4"
|
TeeheeValleyBeforeBirdoDigspot4 = "Teehee Valley Before Birdo Digspot 4"
|
||||||
TeeheeValleyRoom1Digspot1 = "Teehee Valley Room 1 Digspot 1"
|
TeeheeValleyRoom1Digspot1 = "Teehee Valley Room 1 Digspot 1"
|
||||||
TeeheeValleyRoom1Digspot2 = "Teehee Valley Room 1 Digspot 2"
|
TeeheeValleyRoom1Digspot2 = "Teehee Valley Room 1 Digspot 2"
|
||||||
TeeheeValleyRoom1Digspot3 = "Teehee Valley Room 1 Digspot 3"
|
TeeheeValleyRoom1Digspot3 = "Teehee Valley Room 1 Digspot 3"
|
||||||
@@ -296,9 +296,9 @@ class LocationName:
|
|||||||
TeeheeValleyPastUltraHammersDigspot2 = "Teehee Valley Past Ultra Hammer Rock Digspot 2 (Post-Birdo)"
|
TeeheeValleyPastUltraHammersDigspot2 = "Teehee Valley Past Ultra Hammer Rock Digspot 2 (Post-Birdo)"
|
||||||
TeeheeValleyPastUltraHammersDigspot3 = "Teehee Valley Past Ultra Hammer Rock Digspot 3"
|
TeeheeValleyPastUltraHammersDigspot3 = "Teehee Valley Past Ultra Hammer Rock Digspot 3"
|
||||||
TeeheeValleyEntranceToHoohooMountainDigspot = "Teehee Valley Entrance To Hoohoo Mountain Digspot"
|
TeeheeValleyEntranceToHoohooMountainDigspot = "Teehee Valley Entrance To Hoohoo Mountain Digspot"
|
||||||
TeeheeValleySoloLuigiMazeRoom2Digspot1 = "Teehee Valley Solo Luigi Maze Room 2 Digspot 1"
|
TeeheeValleyUpperMazeRoom2Digspot1 = "Teehee Valley Upper Maze Room 2 Digspot 1"
|
||||||
TeeheeValleySoloLuigiMazeRoom2Digspot2 = "Teehee Valley Solo Luigi Maze Room 2 Digspot 2"
|
TeeheeValleyUpperMazeRoom2Digspot2 = "Teehee Valley Upper Maze Room 2 Digspot 2"
|
||||||
TeeheeValleySoloLuigiMazeRoom1Block = "Teehee Valley Solo Luigi Maze Room 1 Block"
|
TeeheeValleyUpperMazeRoom1Block = "Teehee Valley Upper Maze Room 1 Block"
|
||||||
TeeheeValleyBeforeTrunkleDigspot = "Teehee Valley Before Trunkle Digspot"
|
TeeheeValleyBeforeTrunkleDigspot = "Teehee Valley Before Trunkle Digspot"
|
||||||
TeeheeValleyTrunkleRoomDigspot = "Teehee Valley Trunkle Room Digspot"
|
TeeheeValleyTrunkleRoomDigspot = "Teehee Valley Trunkle Room Digspot"
|
||||||
SSChuckolaStorageRoomBlock1 = "S.S. Chuckola Storage Room Block 1"
|
SSChuckolaStorageRoomBlock1 = "S.S. Chuckola Storage Room Block 1"
|
||||||
@@ -314,10 +314,10 @@ class LocationName:
|
|||||||
JokesEndFurnaceRoom1Block1 = "Joke's End Furnace Room 1 Block 1"
|
JokesEndFurnaceRoom1Block1 = "Joke's End Furnace Room 1 Block 1"
|
||||||
JokesEndFurnaceRoom1Block2 = "Joke's End Furnace Room 1 Block 2"
|
JokesEndFurnaceRoom1Block2 = "Joke's End Furnace Room 1 Block 2"
|
||||||
JokesEndFurnaceRoom1Block3 = "Joke's End Furnace Room 1 Block 3"
|
JokesEndFurnaceRoom1Block3 = "Joke's End Furnace Room 1 Block 3"
|
||||||
JokesEndNortheastOfBoilerRoom1Block = "Joke's End Northeast Of Boiler Room 1 Block"
|
JokesEndNortheastOfBoilerRoom1Block = "Joke's End Northeast of Boiler Room 1 Block"
|
||||||
JokesEndNortheastOfBoilerRoom3Digspot = "Joke's End Northeast Of Boiler Room 3 Digspot"
|
JokesEndNortheastOfBoilerRoom3Digspot = "Joke's End Northeast of Boiler Room 3 Digspot"
|
||||||
JokesEndNortheastOfBoilerRoom2Block1 = "Joke's End Northeast Of Boiler Room 2 Block"
|
JokesEndNortheastOfBoilerRoom2Block1 = "Joke's End Northeast of Boiler Room 2 Block"
|
||||||
JokesEndNortheastOfBoilerRoom2Block2 = "Joke's End Northeast Of Boiler Room 2 Digspot"
|
JokesEndNortheastOfBoilerRoom2Digspot = "Joke's End Northeast of Boiler Room 2 Digspot"
|
||||||
JokesEndSecondFloorWestRoomBlock1 = "Joke's End Second Floor West Room Block 1"
|
JokesEndSecondFloorWestRoomBlock1 = "Joke's End Second Floor West Room Block 1"
|
||||||
JokesEndSecondFloorWestRoomBlock2 = "Joke's End Second Floor West Room Block 2"
|
JokesEndSecondFloorWestRoomBlock2 = "Joke's End Second Floor West Room Block 2"
|
||||||
JokesEndSecondFloorWestRoomBlock3 = "Joke's End Second Floor West Room Block 3"
|
JokesEndSecondFloorWestRoomBlock3 = "Joke's End Second Floor West Room Block 3"
|
||||||
@@ -505,7 +505,7 @@ class LocationName:
|
|||||||
BowsersCastleIggyMortonHallwayBlock1 = "Bowser's Castle Iggy & Morton Hallway Block 1"
|
BowsersCastleIggyMortonHallwayBlock1 = "Bowser's Castle Iggy & Morton Hallway Block 1"
|
||||||
BowsersCastleIggyMortonHallwayBlock2 = "Bowser's Castle Iggy & Morton Hallway Block 2"
|
BowsersCastleIggyMortonHallwayBlock2 = "Bowser's Castle Iggy & Morton Hallway Block 2"
|
||||||
BowsersCastleIggyMortonHallwayDigspot = "Bowser's Castle Iggy & Morton Hallway Digspot"
|
BowsersCastleIggyMortonHallwayDigspot = "Bowser's Castle Iggy & Morton Hallway Digspot"
|
||||||
BowsersCastleAfterMortonBlock = "Bowser's Castle After Morton Block"
|
BowsersCastlePastMortonBlock = "Bowser's Castle Past Morton Block"
|
||||||
BowsersCastleLudwigRoyHallwayBlock1 = "Bowser's Castle Ludwig & Roy Hallway Block 1"
|
BowsersCastleLudwigRoyHallwayBlock1 = "Bowser's Castle Ludwig & Roy Hallway Block 1"
|
||||||
BowsersCastleLudwigRoyHallwayBlock2 = "Bowser's Castle Ludwig & Roy Hallway Block 2"
|
BowsersCastleLudwigRoyHallwayBlock2 = "Bowser's Castle Ludwig & Roy Hallway Block 2"
|
||||||
BowsersCastleRoyCorridorBlock1 = "Bowser's Castle Roy Corridor Block 1"
|
BowsersCastleRoyCorridorBlock1 = "Bowser's Castle Roy Corridor Block 1"
|
||||||
@@ -546,7 +546,7 @@ class LocationName:
|
|||||||
ChucklehuckWoodsCaveRoom3CoinBlock = "Chucklehuck Woods Cave Room 3 Coin Block"
|
ChucklehuckWoodsCaveRoom3CoinBlock = "Chucklehuck Woods Cave Room 3 Coin Block"
|
||||||
ChucklehuckWoodsPipe5RoomCoinBlock = "Chucklehuck Woods Pipe 5 Room Coin Block"
|
ChucklehuckWoodsPipe5RoomCoinBlock = "Chucklehuck Woods Pipe 5 Room Coin Block"
|
||||||
ChucklehuckWoodsRoom7CoinBlock = "Chucklehuck Woods Room 7 Coin Block"
|
ChucklehuckWoodsRoom7CoinBlock = "Chucklehuck Woods Room 7 Coin Block"
|
||||||
ChucklehuckWoodsAfterChucklerootCoinBlock = "Chucklehuck Woods After Chuckleroot Coin Block"
|
ChucklehuckWoodsPastChucklerootCoinBlock = "Chucklehuck Woods Past Chuckleroot Coin Block"
|
||||||
ChucklehuckWoodsKoopaRoomCoinBlock = "Chucklehuck Woods Koopa Room Coin Block"
|
ChucklehuckWoodsKoopaRoomCoinBlock = "Chucklehuck Woods Koopa Room Coin Block"
|
||||||
ChucklehuckWoodsWinkleAreaCaveCoinBlock = "Chucklehuck Woods Winkle Area Cave Coin Block"
|
ChucklehuckWoodsWinkleAreaCaveCoinBlock = "Chucklehuck Woods Winkle Area Cave Coin Block"
|
||||||
SewersPrisonRoomCoinBlock = "Sewers Prison Room Coin Block"
|
SewersPrisonRoomCoinBlock = "Sewers Prison Room Coin Block"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from Options import Choice, Toggle, StartInventoryPool, PerGameCommonOptions, Range
|
from Options import Choice, Toggle, StartInventoryPool, PerGameCommonOptions, Range, Removed
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
|
||||||
@@ -282,7 +282,8 @@ class MLSSOptions(PerGameCommonOptions):
|
|||||||
extra_pipes: ExtraPipes
|
extra_pipes: ExtraPipes
|
||||||
skip_minecart: SkipMinecart
|
skip_minecart: SkipMinecart
|
||||||
disable_surf: DisableSurf
|
disable_surf: DisableSurf
|
||||||
harhalls_pants: HarhallsPants
|
disable_harhalls_pants: HarhallsPants
|
||||||
|
harhalls_pants: Removed
|
||||||
block_visibility: HiddenVisible
|
block_visibility: HiddenVisible
|
||||||
chuckle_beans: ChuckleBeans
|
chuckle_beans: ChuckleBeans
|
||||||
music_options: MusicOptions
|
music_options: MusicOptions
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ from .Locations import (
|
|||||||
postJokes,
|
postJokes,
|
||||||
baseUltraRocks,
|
baseUltraRocks,
|
||||||
coins,
|
coins,
|
||||||
|
cacklettas_soul,
|
||||||
)
|
)
|
||||||
from . import StateLogic
|
from . import StateLogic
|
||||||
|
|
||||||
@@ -40,44 +41,45 @@ if typing.TYPE_CHECKING:
|
|||||||
from . import MLSSWorld
|
from . import MLSSWorld
|
||||||
|
|
||||||
|
|
||||||
def create_regions(world: "MLSSWorld", excluded: typing.List[str]):
|
def create_regions(world: "MLSSWorld"):
|
||||||
menu_region = Region("Menu", world.player, world.multiworld)
|
menu_region = Region("Menu", world.player, world.multiworld)
|
||||||
world.multiworld.regions.append(menu_region)
|
world.multiworld.regions.append(menu_region)
|
||||||
|
|
||||||
create_region(world, "Main Area", mainArea, excluded)
|
create_region(world, "Main Area", mainArea)
|
||||||
create_region(world, "Chucklehuck Woods", chucklehuck, excluded)
|
create_region(world, "Chucklehuck Woods", chucklehuck)
|
||||||
create_region(world, "Beanbean Castle Town", castleTown, excluded)
|
create_region(world, "Beanbean Castle Town", castleTown)
|
||||||
create_region(world, "Shop Starting Flag", startingFlag, excluded)
|
create_region(world, "Shop Starting Flag", startingFlag)
|
||||||
create_region(world, "Shop Chuckolator Flag", chuckolatorFlag, excluded)
|
create_region(world, "Shop Chuckolator Flag", chuckolatorFlag)
|
||||||
create_region(world, "Shop Mom Piranha Flag", piranhaFlag, excluded)
|
create_region(world, "Shop Mom Piranha Flag", piranhaFlag)
|
||||||
create_region(world, "Shop Enter Fungitown Flag", kidnappedFlag, excluded)
|
create_region(world, "Shop Enter Fungitown Flag", kidnappedFlag)
|
||||||
create_region(world, "Shop Beanstar Complete Flag", beanstarFlag, excluded)
|
create_region(world, "Shop Beanstar Complete Flag", beanstarFlag)
|
||||||
create_region(world, "Shop Birdo Flag", birdoFlag, excluded)
|
create_region(world, "Shop Birdo Flag", birdoFlag)
|
||||||
create_region(world, "Surfable", surfable, excluded)
|
create_region(world, "Surfable", surfable)
|
||||||
create_region(world, "Hooniversity", hooniversity, excluded)
|
create_region(world, "Hooniversity", hooniversity)
|
||||||
create_region(world, "GwarharEntrance", gwarharEntrance, excluded)
|
create_region(world, "GwarharEntrance", gwarharEntrance)
|
||||||
create_region(world, "GwarharMain", gwarharMain, excluded)
|
create_region(world, "GwarharMain", gwarharMain)
|
||||||
create_region(world, "TeeheeValley", teeheeValley, excluded)
|
create_region(world, "TeeheeValley", teeheeValley)
|
||||||
create_region(world, "Winkle", winkle, excluded)
|
create_region(world, "Winkle", winkle)
|
||||||
create_region(world, "Sewers", sewers, excluded)
|
create_region(world, "Sewers", sewers)
|
||||||
create_region(world, "Airport", airport, excluded)
|
create_region(world, "Airport", airport)
|
||||||
create_region(world, "JokesEntrance", jokesEntrance, excluded)
|
create_region(world, "JokesEntrance", jokesEntrance)
|
||||||
create_region(world, "JokesMain", jokesMain, excluded)
|
create_region(world, "JokesMain", jokesMain)
|
||||||
create_region(world, "PostJokes", postJokes, excluded)
|
create_region(world, "PostJokes", postJokes)
|
||||||
create_region(world, "Theater", theater, excluded)
|
create_region(world, "Theater", theater)
|
||||||
create_region(world, "Fungitown", fungitown, excluded)
|
create_region(world, "Fungitown", fungitown)
|
||||||
create_region(world, "Fungitown Shop Beanstar Complete Flag", fungitownBeanstar, excluded)
|
create_region(world, "Fungitown Shop Beanstar Complete Flag", fungitownBeanstar)
|
||||||
create_region(world, "Fungitown Shop Birdo Flag", fungitownBirdo, excluded)
|
create_region(world, "Fungitown Shop Birdo Flag", fungitownBirdo)
|
||||||
create_region(world, "BooStatue", booStatue, excluded)
|
create_region(world, "BooStatue", booStatue)
|
||||||
create_region(world, "Oasis", oasis, excluded)
|
create_region(world, "Oasis", oasis)
|
||||||
create_region(world, "BaseUltraRocks", baseUltraRocks, excluded)
|
create_region(world, "BaseUltraRocks", baseUltraRocks)
|
||||||
|
create_region(world, "Cackletta's Soul", cacklettas_soul)
|
||||||
|
|
||||||
if world.options.coins:
|
if world.options.coins:
|
||||||
create_region(world, "Coins", coins, excluded)
|
create_region(world, "Coins", coins)
|
||||||
|
|
||||||
if not world.options.castle_skip:
|
if not world.options.castle_skip:
|
||||||
create_region(world, "Bowser's Castle", bowsers, excluded)
|
create_region(world, "Bowser's Castle", bowsers)
|
||||||
create_region(world, "Bowser's Castle Mini", bowsersMini, excluded)
|
create_region(world, "Bowser's Castle Mini", bowsersMini)
|
||||||
|
|
||||||
|
|
||||||
def connect_regions(world: "MLSSWorld"):
|
def connect_regions(world: "MLSSWorld"):
|
||||||
@@ -221,6 +223,9 @@ def connect_regions(world: "MLSSWorld"):
|
|||||||
"Bowser's Castle Mini",
|
"Bowser's Castle Mini",
|
||||||
lambda state: StateLogic.canMini(state, world.player) and StateLogic.thunder(state, world.player),
|
lambda state: StateLogic.canMini(state, world.player) and StateLogic.thunder(state, world.player),
|
||||||
)
|
)
|
||||||
|
connect(world, names, "Bowser's Castle Mini", "Cackletta's Soul")
|
||||||
|
else:
|
||||||
|
connect(world, names, "PostJokes", "Cackletta's Soul")
|
||||||
connect(world, names, "Chucklehuck Woods", "Winkle", lambda state: StateLogic.canDash(state, world.player))
|
connect(world, names, "Chucklehuck Woods", "Winkle", lambda state: StateLogic.canDash(state, world.player))
|
||||||
connect(
|
connect(
|
||||||
world,
|
world,
|
||||||
@@ -282,11 +287,11 @@ def connect_regions(world: "MLSSWorld"):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def create_region(world: "MLSSWorld", name, locations, excluded):
|
def create_region(world: "MLSSWorld", name, locations):
|
||||||
ret = Region(name, world.player, world.multiworld)
|
ret = Region(name, world.player, world.multiworld)
|
||||||
for location in locations:
|
for location in locations:
|
||||||
loc = MLSSLocation(world.player, location.name, location.id, ret)
|
loc = MLSSLocation(world.player, location.name, location.id, ret)
|
||||||
if location.name in excluded:
|
if location.name in world.disabled_locations:
|
||||||
continue
|
continue
|
||||||
ret.locations.append(loc)
|
ret.locations.append(loc)
|
||||||
world.multiworld.regions.append(ret)
|
world.multiworld.regions.append(ret)
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from BaseClasses import Item, Location
|
|||||||
from settings import get_settings
|
from settings import get_settings
|
||||||
from worlds.Files import APProcedurePatch, APTokenMixin, APTokenTypes, APPatchExtension
|
from worlds.Files import APProcedurePatch, APTokenMixin, APTokenTypes, APPatchExtension
|
||||||
from .Items import item_table
|
from .Items import item_table
|
||||||
from .Locations import shop, badge, pants, location_table, hidden, all_locations
|
from .Locations import shop, badge, pants, location_table, all_locations
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from . import MLSSWorld
|
from . import MLSSWorld
|
||||||
@@ -88,7 +88,7 @@ class MLSSPatchExtension(APPatchExtension):
|
|||||||
return rom
|
return rom
|
||||||
stream = io.BytesIO(rom)
|
stream = io.BytesIO(rom)
|
||||||
|
|
||||||
for location in all_locations:
|
for location in [location for location in all_locations if location.itemType == 0]:
|
||||||
stream.seek(location.id - 6)
|
stream.seek(location.id - 6)
|
||||||
b = stream.read(1)
|
b = stream.read(1)
|
||||||
if b[0] == 0x10 and options["block_visibility"] == 1:
|
if b[0] == 0x10 and options["block_visibility"] == 1:
|
||||||
@@ -133,7 +133,7 @@ class MLSSPatchExtension(APPatchExtension):
|
|||||||
stream = io.BytesIO(rom)
|
stream = io.BytesIO(rom)
|
||||||
random.seed(options["seed"] + options["player"])
|
random.seed(options["seed"] + options["player"])
|
||||||
|
|
||||||
if options["randomize_bosses"] == 1 or (options["randomize_bosses"] == 2) and options["randomize_enemies"] == 0:
|
if options["randomize_bosses"] == 1 or (options["randomize_bosses"] == 2 and options["randomize_enemies"] == 0):
|
||||||
raw = []
|
raw = []
|
||||||
for pos in bosses:
|
for pos in bosses:
|
||||||
stream.seek(pos + 1)
|
stream.seek(pos + 1)
|
||||||
@@ -164,6 +164,7 @@ class MLSSPatchExtension(APPatchExtension):
|
|||||||
|
|
||||||
enemies_raw = []
|
enemies_raw = []
|
||||||
groups = []
|
groups = []
|
||||||
|
boss_groups = []
|
||||||
|
|
||||||
if options["randomize_enemies"] == 0:
|
if options["randomize_enemies"] == 0:
|
||||||
return stream.getvalue()
|
return stream.getvalue()
|
||||||
@@ -171,7 +172,7 @@ class MLSSPatchExtension(APPatchExtension):
|
|||||||
if options["randomize_bosses"] == 2:
|
if options["randomize_bosses"] == 2:
|
||||||
for pos in bosses:
|
for pos in bosses:
|
||||||
stream.seek(pos + 1)
|
stream.seek(pos + 1)
|
||||||
groups += [stream.read(0x1F)]
|
boss_groups += [stream.read(0x1F)]
|
||||||
|
|
||||||
for pos in enemies:
|
for pos in enemies:
|
||||||
stream.seek(pos + 8)
|
stream.seek(pos + 8)
|
||||||
@@ -221,12 +222,19 @@ class MLSSPatchExtension(APPatchExtension):
|
|||||||
groups += [raw]
|
groups += [raw]
|
||||||
chomp = False
|
chomp = False
|
||||||
|
|
||||||
random.shuffle(groups)
|
|
||||||
arr = enemies
|
arr = enemies
|
||||||
if options["randomize_bosses"] == 2:
|
if options["randomize_bosses"] == 2:
|
||||||
arr += bosses
|
arr += bosses
|
||||||
|
groups += boss_groups
|
||||||
|
|
||||||
|
random.shuffle(groups)
|
||||||
|
|
||||||
for pos in arr:
|
for pos in arr:
|
||||||
|
if arr[-1] in boss_groups:
|
||||||
|
stream.seek(pos)
|
||||||
|
temp = stream.read(1)
|
||||||
|
stream.seek(pos)
|
||||||
|
stream.write(bytes([temp[0] | 0x8]))
|
||||||
stream.seek(pos + 1)
|
stream.seek(pos + 1)
|
||||||
stream.write(groups.pop())
|
stream.write(groups.pop())
|
||||||
|
|
||||||
@@ -320,20 +328,9 @@ def write_tokens(world: "MLSSWorld", patch: MLSSProcedurePatch) -> None:
|
|||||||
patch.write_token(APTokenTypes.WRITE, address + 3, bytes([world.random.randint(0x0, 0x26)]))
|
patch.write_token(APTokenTypes.WRITE, address + 3, bytes([world.random.randint(0x0, 0x26)]))
|
||||||
|
|
||||||
for location_name in location_table.keys():
|
for location_name in location_table.keys():
|
||||||
if (
|
if location_name in world.disabled_locations:
|
||||||
(world.options.skip_minecart and "Minecart" in location_name and "After" not in location_name)
|
|
||||||
or (world.options.castle_skip and "Bowser" in location_name)
|
|
||||||
or (world.options.disable_surf and "Surf Minigame" in location_name)
|
|
||||||
or (world.options.harhalls_pants and "Harhall's" in location_name)
|
|
||||||
):
|
|
||||||
continue
|
continue
|
||||||
if (world.options.chuckle_beans == 0 and "Digspot" in location_name) or (
|
location = world.get_location(location_name)
|
||||||
world.options.chuckle_beans == 1 and location_table[location_name] in hidden
|
|
||||||
):
|
|
||||||
continue
|
|
||||||
if not world.options.coins and "Coin" in location_name:
|
|
||||||
continue
|
|
||||||
location = world.multiworld.get_location(location_name, world.player)
|
|
||||||
item = location.item
|
item = location.item
|
||||||
address = [address for address in all_locations if address.name == location.name]
|
address = [address for address in all_locations if address.name == location.name]
|
||||||
item_inject(world, patch, location.address, address[0].itemType, item)
|
item_inject(world, patch, location.address, address[0].itemType, item)
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ def set_rules(world: "MLSSWorld", excluded):
|
|||||||
for location in all_locations:
|
for location in all_locations:
|
||||||
if "Digspot" in location.name:
|
if "Digspot" in location.name:
|
||||||
if (world.options.skip_minecart and "Minecart" in location.name) or (
|
if (world.options.skip_minecart and "Minecart" in location.name) or (
|
||||||
world.options.castle_skip and "Bowser" in location.name
|
world.options.castle_skip and "Bowser" in location.name
|
||||||
):
|
):
|
||||||
continue
|
continue
|
||||||
if world.options.chuckle_beans == 0 or world.options.chuckle_beans == 1 and location.id in hidden:
|
if world.options.chuckle_beans == 0 or world.options.chuckle_beans == 1 and location.id in hidden:
|
||||||
@@ -218,9 +218,9 @@ def set_rules(world: "MLSSWorld", excluded):
|
|||||||
add_rule(
|
add_rule(
|
||||||
world.get_location(LocationName.BeanbeanOutskirtsUltraHammerUpgrade),
|
world.get_location(LocationName.BeanbeanOutskirtsUltraHammerUpgrade),
|
||||||
lambda state: StateLogic.thunder(state, world.player)
|
lambda state: StateLogic.thunder(state, world.player)
|
||||||
and StateLogic.pieces(state, world.player)
|
and StateLogic.pieces(state, world.player)
|
||||||
and StateLogic.castleTown(state, world.player)
|
and StateLogic.castleTown(state, world.player)
|
||||||
and StateLogic.rose(state, world.player),
|
and StateLogic.rose(state, world.player),
|
||||||
)
|
)
|
||||||
add_rule(
|
add_rule(
|
||||||
world.get_location(LocationName.BeanbeanOutskirtsSoloLuigiCaveMole),
|
world.get_location(LocationName.BeanbeanOutskirtsSoloLuigiCaveMole),
|
||||||
@@ -235,27 +235,27 @@ def set_rules(world: "MLSSWorld", excluded):
|
|||||||
lambda state: StateLogic.canDig(state, world.player) and StateLogic.canMini(state, world.player),
|
lambda state: StateLogic.canDig(state, world.player) and StateLogic.canMini(state, world.player),
|
||||||
)
|
)
|
||||||
add_rule(
|
add_rule(
|
||||||
world.get_location(LocationName.ChucklehuckWoodsAfterChucklerootBlock1),
|
world.get_location(LocationName.ChucklehuckWoodsPastChucklerootBlock1),
|
||||||
lambda state: StateLogic.fruits(state, world.player),
|
lambda state: StateLogic.fruits(state, world.player),
|
||||||
)
|
)
|
||||||
add_rule(
|
add_rule(
|
||||||
world.get_location(LocationName.ChucklehuckWoodsAfterChucklerootBlock2),
|
world.get_location(LocationName.ChucklehuckWoodsPastChucklerootBlock2),
|
||||||
lambda state: StateLogic.fruits(state, world.player),
|
lambda state: StateLogic.fruits(state, world.player),
|
||||||
)
|
)
|
||||||
add_rule(
|
add_rule(
|
||||||
world.get_location(LocationName.ChucklehuckWoodsAfterChucklerootBlock3),
|
world.get_location(LocationName.ChucklehuckWoodsPastChucklerootBlock3),
|
||||||
lambda state: StateLogic.fruits(state, world.player),
|
lambda state: StateLogic.fruits(state, world.player),
|
||||||
)
|
)
|
||||||
add_rule(
|
add_rule(
|
||||||
world.get_location(LocationName.ChucklehuckWoodsAfterChucklerootBlock4),
|
world.get_location(LocationName.ChucklehuckWoodsPastChucklerootBlock4),
|
||||||
lambda state: StateLogic.fruits(state, world.player),
|
lambda state: StateLogic.fruits(state, world.player),
|
||||||
)
|
)
|
||||||
add_rule(
|
add_rule(
|
||||||
world.get_location(LocationName.ChucklehuckWoodsAfterChucklerootBlock5),
|
world.get_location(LocationName.ChucklehuckWoodsPastChucklerootBlock5),
|
||||||
lambda state: StateLogic.fruits(state, world.player),
|
lambda state: StateLogic.fruits(state, world.player),
|
||||||
)
|
)
|
||||||
add_rule(
|
add_rule(
|
||||||
world.get_location(LocationName.ChucklehuckWoodsAfterChucklerootBlock6),
|
world.get_location(LocationName.ChucklehuckWoodsPastChucklerootBlock6),
|
||||||
lambda state: StateLogic.fruits(state, world.player),
|
lambda state: StateLogic.fruits(state, world.player),
|
||||||
)
|
)
|
||||||
add_rule(
|
add_rule(
|
||||||
@@ -350,10 +350,6 @@ def set_rules(world: "MLSSWorld", excluded):
|
|||||||
world.get_location(LocationName.TeeheeValleyPastUltraHammersBlock2),
|
world.get_location(LocationName.TeeheeValleyPastUltraHammersBlock2),
|
||||||
lambda state: StateLogic.ultra(state, world.player),
|
lambda state: StateLogic.ultra(state, world.player),
|
||||||
)
|
)
|
||||||
add_rule(
|
|
||||||
world.get_location(LocationName.TeeheeValleySoloLuigiMazeRoom1Block),
|
|
||||||
lambda state: StateLogic.ultra(state, world.player),
|
|
||||||
)
|
|
||||||
add_rule(
|
add_rule(
|
||||||
world.get_location(LocationName.OhoOasisFirebrand),
|
world.get_location(LocationName.OhoOasisFirebrand),
|
||||||
lambda state: StateLogic.canMini(state, world.player),
|
lambda state: StateLogic.canMini(state, world.player),
|
||||||
@@ -462,6 +458,143 @@ def set_rules(world: "MLSSWorld", excluded):
|
|||||||
lambda state: StateLogic.canCrash(state, world.player),
|
lambda state: StateLogic.canCrash(state, world.player),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if world.options.randomize_bosses.value != 0:
|
||||||
|
if world.options.chuckle_beans != 0:
|
||||||
|
add_rule(
|
||||||
|
world.get_location(LocationName.HoohooMountainHoohoorosRoomDigspot1),
|
||||||
|
lambda state: StateLogic.hammers(state, world.player)
|
||||||
|
or StateLogic.fire(state, world.player)
|
||||||
|
or StateLogic.thunder(state, world.player),
|
||||||
|
)
|
||||||
|
add_rule(
|
||||||
|
world.get_location(LocationName.HoohooMountainPastHoohoorosDigspot),
|
||||||
|
lambda state: StateLogic.hammers(state, world.player)
|
||||||
|
or StateLogic.fire(state, world.player)
|
||||||
|
or StateLogic.thunder(state, world.player),
|
||||||
|
)
|
||||||
|
add_rule(
|
||||||
|
world.get_location(LocationName.HoohooMountainPastHoohoorosConnectorRoomDigspot1),
|
||||||
|
lambda state: StateLogic.hammers(state, world.player)
|
||||||
|
or StateLogic.fire(state, world.player)
|
||||||
|
or StateLogic.thunder(state, world.player),
|
||||||
|
)
|
||||||
|
add_rule(
|
||||||
|
world.get_location(LocationName.HoohooMountainBelowSummitDigspot),
|
||||||
|
lambda state: StateLogic.hammers(state, world.player)
|
||||||
|
or StateLogic.fire(state, world.player)
|
||||||
|
or StateLogic.thunder(state, world.player),
|
||||||
|
)
|
||||||
|
add_rule(
|
||||||
|
world.get_location(LocationName.HoohooMountainSummitDigspot),
|
||||||
|
lambda state: StateLogic.hammers(state, world.player)
|
||||||
|
or StateLogic.fire(state, world.player)
|
||||||
|
or StateLogic.thunder(state, world.player),
|
||||||
|
)
|
||||||
|
if world.options.chuckle_beans == 2:
|
||||||
|
add_rule(
|
||||||
|
world.get_location(LocationName.HoohooMountainHoohoorosRoomDigspot2),
|
||||||
|
lambda state: StateLogic.hammers(state, world.player)
|
||||||
|
or StateLogic.fire(state, world.player)
|
||||||
|
or StateLogic.thunder(state, world.player),
|
||||||
|
)
|
||||||
|
add_rule(
|
||||||
|
world.get_location(LocationName.HoohooMountainPastHoohoorosConnectorRoomDigspot2),
|
||||||
|
lambda state: StateLogic.hammers(state, world.player)
|
||||||
|
or StateLogic.fire(state, world.player)
|
||||||
|
or StateLogic.thunder(state, world.player),
|
||||||
|
)
|
||||||
|
add_rule(
|
||||||
|
world.get_location(LocationName.HoohooVillageHammers),
|
||||||
|
lambda state: StateLogic.hammers(state, world.player)
|
||||||
|
or StateLogic.fire(state, world.player)
|
||||||
|
or StateLogic.thunder(state, world.player),
|
||||||
|
)
|
||||||
|
add_rule(
|
||||||
|
world.get_location(LocationName.HoohooMountainPeasleysRose),
|
||||||
|
lambda state: StateLogic.hammers(state, world.player)
|
||||||
|
or StateLogic.fire(state, world.player)
|
||||||
|
or StateLogic.thunder(state, world.player),
|
||||||
|
)
|
||||||
|
add_rule(
|
||||||
|
world.get_location(LocationName.HoohooMountainHoohoorosRoomBlock1),
|
||||||
|
lambda state: StateLogic.hammers(state, world.player)
|
||||||
|
or StateLogic.fire(state, world.player)
|
||||||
|
or StateLogic.thunder(state, world.player),
|
||||||
|
)
|
||||||
|
add_rule(
|
||||||
|
world.get_location(LocationName.HoohooMountainHoohoorosRoomBlock2),
|
||||||
|
lambda state: StateLogic.hammers(state, world.player)
|
||||||
|
or StateLogic.fire(state, world.player)
|
||||||
|
or StateLogic.thunder(state, world.player),
|
||||||
|
)
|
||||||
|
add_rule(
|
||||||
|
world.get_location(LocationName.HoohooMountainBelowSummitBlock1),
|
||||||
|
lambda state: StateLogic.hammers(state, world.player)
|
||||||
|
or StateLogic.fire(state, world.player)
|
||||||
|
or StateLogic.thunder(state, world.player),
|
||||||
|
)
|
||||||
|
add_rule(
|
||||||
|
world.get_location(LocationName.HoohooMountainBelowSummitBlock2),
|
||||||
|
lambda state: StateLogic.hammers(state, world.player)
|
||||||
|
or StateLogic.fire(state, world.player)
|
||||||
|
or StateLogic.thunder(state, world.player),
|
||||||
|
)
|
||||||
|
add_rule(
|
||||||
|
world.get_location(LocationName.HoohooMountainBelowSummitBlock3),
|
||||||
|
lambda state: StateLogic.hammers(state, world.player)
|
||||||
|
or StateLogic.fire(state, world.player)
|
||||||
|
or StateLogic.thunder(state, world.player),
|
||||||
|
)
|
||||||
|
add_rule(
|
||||||
|
world.get_location(LocationName.HoohooMountainPastHoohoorosBlock1),
|
||||||
|
lambda state: StateLogic.hammers(state, world.player)
|
||||||
|
or StateLogic.fire(state, world.player)
|
||||||
|
or StateLogic.thunder(state, world.player),
|
||||||
|
)
|
||||||
|
add_rule(
|
||||||
|
world.get_location(LocationName.HoohooMountainPastHoohoorosBlock2),
|
||||||
|
lambda state: StateLogic.hammers(state, world.player)
|
||||||
|
or StateLogic.fire(state, world.player)
|
||||||
|
or StateLogic.thunder(state, world.player),
|
||||||
|
)
|
||||||
|
add_rule(
|
||||||
|
world.get_location(LocationName.HoohooMountainPastHoohoorosConnectorRoomBlock),
|
||||||
|
lambda state: StateLogic.hammers(state, world.player)
|
||||||
|
or StateLogic.fire(state, world.player)
|
||||||
|
or StateLogic.thunder(state, world.player),
|
||||||
|
)
|
||||||
|
|
||||||
|
if not world.options.difficult_logic:
|
||||||
|
if world.options.chuckle_beans != 0:
|
||||||
|
add_rule(
|
||||||
|
world.get_location(LocationName.JokesEndNortheastOfBoilerRoom2Digspot),
|
||||||
|
lambda state: StateLogic.canCrash(state, world.player),
|
||||||
|
)
|
||||||
|
add_rule(
|
||||||
|
world.get_location(LocationName.JokesEndNortheastOfBoilerRoom3Digspot),
|
||||||
|
lambda state: StateLogic.canCrash(state, world.player),
|
||||||
|
)
|
||||||
|
add_rule(
|
||||||
|
world.get_location(LocationName.JokesEndNortheastOfBoilerRoom1Block),
|
||||||
|
lambda state: StateLogic.canCrash(state, world.player),
|
||||||
|
)
|
||||||
|
add_rule(
|
||||||
|
world.get_location(LocationName.JokesEndNortheastOfBoilerRoom2Block1),
|
||||||
|
lambda state: StateLogic.canCrash(state, world.player),
|
||||||
|
)
|
||||||
|
add_rule(
|
||||||
|
world.get_location(LocationName.JokesEndFurnaceRoom1Block1),
|
||||||
|
lambda state: StateLogic.canCrash(state, world.player),
|
||||||
|
)
|
||||||
|
add_rule(
|
||||||
|
world.get_location(LocationName.JokesEndFurnaceRoom1Block2),
|
||||||
|
lambda state: StateLogic.canCrash(state, world.player),
|
||||||
|
)
|
||||||
|
add_rule(
|
||||||
|
world.get_location(LocationName.JokesEndFurnaceRoom1Block3),
|
||||||
|
lambda state: StateLogic.canCrash(state, world.player),
|
||||||
|
)
|
||||||
|
|
||||||
if world.options.coins:
|
if world.options.coins:
|
||||||
add_rule(
|
add_rule(
|
||||||
world.get_location(LocationName.HoohooMountainBaseBooStatueCaveCoinBlock1),
|
world.get_location(LocationName.HoohooMountainBaseBooStatueCaveCoinBlock1),
|
||||||
@@ -516,7 +649,7 @@ def set_rules(world: "MLSSWorld", excluded):
|
|||||||
lambda state: StateLogic.brooch(state, world.player) and StateLogic.hammers(state, world.player),
|
lambda state: StateLogic.brooch(state, world.player) and StateLogic.hammers(state, world.player),
|
||||||
)
|
)
|
||||||
add_rule(
|
add_rule(
|
||||||
world.get_location(LocationName.ChucklehuckWoodsAfterChucklerootCoinBlock),
|
world.get_location(LocationName.ChucklehuckWoodsPastChucklerootCoinBlock),
|
||||||
lambda state: StateLogic.brooch(state, world.player) and StateLogic.fruits(state, world.player),
|
lambda state: StateLogic.brooch(state, world.player) and StateLogic.fruits(state, world.player),
|
||||||
)
|
)
|
||||||
add_rule(
|
add_rule(
|
||||||
@@ -546,23 +679,23 @@ def set_rules(world: "MLSSWorld", excluded):
|
|||||||
add_rule(
|
add_rule(
|
||||||
world.get_location(LocationName.GwarharLagoonFirstUnderwaterAreaRoom2CoinBlock),
|
world.get_location(LocationName.GwarharLagoonFirstUnderwaterAreaRoom2CoinBlock),
|
||||||
lambda state: StateLogic.canDash(state, world.player)
|
lambda state: StateLogic.canDash(state, world.player)
|
||||||
and (StateLogic.membership(state, world.player) or StateLogic.surfable(state, world.player)),
|
and (StateLogic.membership(state, world.player) or StateLogic.surfable(state, world.player)),
|
||||||
)
|
)
|
||||||
add_rule(
|
add_rule(
|
||||||
world.get_location(LocationName.JokesEndSecondFloorWestRoomCoinBlock),
|
world.get_location(LocationName.JokesEndSecondFloorWestRoomCoinBlock),
|
||||||
lambda state: StateLogic.ultra(state, world.player)
|
lambda state: StateLogic.ultra(state, world.player)
|
||||||
and StateLogic.fire(state, world.player)
|
and StateLogic.fire(state, world.player)
|
||||||
and (
|
and (StateLogic.membership(state, world.player)
|
||||||
StateLogic.membership(state, world.player)
|
or (StateLogic.canDig(state, world.player)
|
||||||
or (StateLogic.canDig(state, world.player) and StateLogic.canMini(state, world.player))
|
and StateLogic.canMini(state, world.player))),
|
||||||
),
|
|
||||||
)
|
)
|
||||||
add_rule(
|
add_rule(
|
||||||
world.get_location(LocationName.JokesEndNorthofBridgeRoomCoinBlock),
|
world.get_location(LocationName.JokesEndNorthofBridgeRoomCoinBlock),
|
||||||
lambda state: StateLogic.ultra(state, world.player)
|
lambda state: StateLogic.ultra(state, world.player)
|
||||||
and StateLogic.fire(state, world.player)
|
and StateLogic.fire(state, world.player)
|
||||||
and StateLogic.canDig(state, world.player)
|
and StateLogic.canDig(state, world.player)
|
||||||
and (StateLogic.membership(state, world.player) or StateLogic.canMini(state, world.player)),
|
and (StateLogic.membership(state, world.player)
|
||||||
|
or StateLogic.canMini(state, world.player)),
|
||||||
)
|
)
|
||||||
if not world.options.difficult_logic:
|
if not world.options.difficult_logic:
|
||||||
add_rule(
|
add_rule(
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import typing
|
|||||||
import settings
|
import settings
|
||||||
from BaseClasses import Tutorial, ItemClassification
|
from BaseClasses import Tutorial, ItemClassification
|
||||||
from worlds.AutoWorld import WebWorld, World
|
from worlds.AutoWorld import WebWorld, World
|
||||||
from typing import List, Dict, Any
|
from typing import Set, Dict, Any
|
||||||
from .Locations import all_locations, location_table, bowsers, bowsersMini, hidden, coins
|
from .Locations import all_locations, location_table, bowsers, bowsersMini, hidden, coins
|
||||||
from .Options import MLSSOptions
|
from .Options import MLSSOptions
|
||||||
from .Items import MLSSItem, itemList, item_frequencies, item_table
|
from .Items import MLSSItem, itemList, item_frequencies, item_table
|
||||||
@@ -55,29 +55,29 @@ class MLSSWorld(World):
|
|||||||
settings: typing.ClassVar[MLSSSettings]
|
settings: typing.ClassVar[MLSSSettings]
|
||||||
item_name_to_id = {name: data.code for name, data in item_table.items()}
|
item_name_to_id = {name: data.code for name, data in item_table.items()}
|
||||||
location_name_to_id = {loc_data.name: loc_data.id for loc_data in all_locations}
|
location_name_to_id = {loc_data.name: loc_data.id for loc_data in all_locations}
|
||||||
required_client_version = (0, 4, 5)
|
required_client_version = (0, 5, 0)
|
||||||
|
|
||||||
disabled_locations: List[str]
|
disabled_locations: Set[str]
|
||||||
|
|
||||||
def generate_early(self) -> None:
|
def generate_early(self) -> None:
|
||||||
self.disabled_locations = []
|
self.disabled_locations = set()
|
||||||
if self.options.chuckle_beans == 0:
|
|
||||||
self.disabled_locations += [location.name for location in all_locations if "Digspot" in location.name]
|
|
||||||
if self.options.castle_skip:
|
|
||||||
self.disabled_locations += [location.name for location in all_locations if "Bowser" in location.name]
|
|
||||||
if self.options.chuckle_beans == 1:
|
|
||||||
self.disabled_locations = [location.name for location in all_locations if location.id in hidden]
|
|
||||||
if self.options.skip_minecart:
|
if self.options.skip_minecart:
|
||||||
self.disabled_locations += [LocationName.HoohooMountainBaseMinecartCaveDigspot]
|
self.disabled_locations.update([LocationName.HoohooMountainBaseMinecartCaveDigspot])
|
||||||
if self.options.disable_surf:
|
if self.options.disable_surf:
|
||||||
self.disabled_locations += [LocationName.SurfMinigame]
|
self.disabled_locations.update([LocationName.SurfMinigame])
|
||||||
if self.options.harhalls_pants:
|
if self.options.disable_harhalls_pants:
|
||||||
self.disabled_locations += [LocationName.HarhallsPants]
|
self.disabled_locations.update([LocationName.HarhallsPants])
|
||||||
|
if self.options.chuckle_beans == 0:
|
||||||
|
self.disabled_locations.update([location.name for location in all_locations if "Digspot" in location.name])
|
||||||
|
if self.options.chuckle_beans == 1:
|
||||||
|
self.disabled_locations.update([location.name for location in all_locations if location.id in hidden])
|
||||||
|
if self.options.castle_skip:
|
||||||
|
self.disabled_locations.update([location.name for location in bowsers + bowsersMini])
|
||||||
if not self.options.coins:
|
if not self.options.coins:
|
||||||
self.disabled_locations += [location.name for location in all_locations if location in coins]
|
self.disabled_locations.update([location.name for location in coins])
|
||||||
|
|
||||||
def create_regions(self) -> None:
|
def create_regions(self) -> None:
|
||||||
create_regions(self, self.disabled_locations)
|
create_regions(self)
|
||||||
connect_regions(self)
|
connect_regions(self)
|
||||||
|
|
||||||
item = self.create_item("Mushroom")
|
item = self.create_item("Mushroom")
|
||||||
@@ -90,13 +90,15 @@ class MLSSWorld(World):
|
|||||||
self.get_location(LocationName.PantsShopStartingFlag1).place_locked_item(item)
|
self.get_location(LocationName.PantsShopStartingFlag1).place_locked_item(item)
|
||||||
item = self.create_item("Chuckle Bean")
|
item = self.create_item("Chuckle Bean")
|
||||||
self.get_location(LocationName.PantsShopStartingFlag2).place_locked_item(item)
|
self.get_location(LocationName.PantsShopStartingFlag2).place_locked_item(item)
|
||||||
|
item = MLSSItem("Victory", ItemClassification.progression, None, self.player)
|
||||||
|
self.get_location("Cackletta's Soul").place_locked_item(item)
|
||||||
|
|
||||||
def fill_slot_data(self) -> Dict[str, Any]:
|
def fill_slot_data(self) -> Dict[str, Any]:
|
||||||
return {
|
return {
|
||||||
"CastleSkip": self.options.castle_skip.value,
|
"CastleSkip": self.options.castle_skip.value,
|
||||||
"SkipMinecart": self.options.skip_minecart.value,
|
"SkipMinecart": self.options.skip_minecart.value,
|
||||||
"DisableSurf": self.options.disable_surf.value,
|
"DisableSurf": self.options.disable_surf.value,
|
||||||
"HarhallsPants": self.options.harhalls_pants.value,
|
"HarhallsPants": self.options.disable_harhalls_pants.value,
|
||||||
"ChuckleBeans": self.options.chuckle_beans.value,
|
"ChuckleBeans": self.options.chuckle_beans.value,
|
||||||
"DifficultLogic": self.options.difficult_logic.value,
|
"DifficultLogic": self.options.difficult_logic.value,
|
||||||
"Coins": self.options.coins.value,
|
"Coins": self.options.coins.value,
|
||||||
@@ -111,7 +113,7 @@ class MLSSWorld(World):
|
|||||||
freq = item_frequencies.get(item.itemName, 1)
|
freq = item_frequencies.get(item.itemName, 1)
|
||||||
if item in precollected:
|
if item in precollected:
|
||||||
freq = max(freq - precollected.count(item), 0)
|
freq = max(freq - precollected.count(item), 0)
|
||||||
if self.options.harhalls_pants and "Harhall's" in item.itemName:
|
if self.options.disable_harhalls_pants and "Harhall's" in item.itemName:
|
||||||
continue
|
continue
|
||||||
required_items += [item.itemName for _ in range(freq)]
|
required_items += [item.itemName for _ in range(freq)]
|
||||||
|
|
||||||
@@ -135,21 +137,7 @@ class MLSSWorld(World):
|
|||||||
filler_items += [item.itemName for _ in range(freq)]
|
filler_items += [item.itemName for _ in range(freq)]
|
||||||
|
|
||||||
# And finally take as many fillers as we need to have the same amount of items and locations.
|
# And finally take as many fillers as we need to have the same amount of items and locations.
|
||||||
remaining = len(all_locations) - len(required_items) - 5
|
remaining = len(all_locations) - len(required_items) - len(self.disabled_locations) - 5
|
||||||
if self.options.castle_skip:
|
|
||||||
remaining -= len(bowsers) + len(bowsersMini) - (5 if self.options.chuckle_beans == 0 else 0)
|
|
||||||
if self.options.skip_minecart and self.options.chuckle_beans == 2:
|
|
||||||
remaining -= 1
|
|
||||||
if self.options.disable_surf:
|
|
||||||
remaining -= 1
|
|
||||||
if self.options.harhalls_pants:
|
|
||||||
remaining -= 1
|
|
||||||
if self.options.chuckle_beans == 0:
|
|
||||||
remaining -= 192
|
|
||||||
if self.options.chuckle_beans == 1:
|
|
||||||
remaining -= 59
|
|
||||||
if not self.options.coins:
|
|
||||||
remaining -= len(coins)
|
|
||||||
|
|
||||||
self.multiworld.itempool += [
|
self.multiworld.itempool += [
|
||||||
self.create_item(filler_item_name) for filler_item_name in self.random.sample(filler_items, remaining)
|
self.create_item(filler_item_name) for filler_item_name in self.random.sample(filler_items, remaining)
|
||||||
@@ -157,21 +145,14 @@ class MLSSWorld(World):
|
|||||||
|
|
||||||
def set_rules(self) -> None:
|
def set_rules(self) -> None:
|
||||||
set_rules(self, self.disabled_locations)
|
set_rules(self, self.disabled_locations)
|
||||||
if self.options.castle_skip:
|
self.multiworld.completion_condition[self.player] = lambda state: state.has("Victory", self.player)
|
||||||
self.multiworld.completion_condition[self.player] = lambda state: state.can_reach(
|
|
||||||
"PostJokes", "Region", self.player
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
self.multiworld.completion_condition[self.player] = lambda state: state.can_reach(
|
|
||||||
"Bowser's Castle Mini", "Region", self.player
|
|
||||||
)
|
|
||||||
|
|
||||||
def create_item(self, name: str) -> MLSSItem:
|
def create_item(self, name: str) -> MLSSItem:
|
||||||
item = item_table[name]
|
item = item_table[name]
|
||||||
return MLSSItem(item.itemName, item.classification, item.code, self.player)
|
return MLSSItem(item.itemName, item.classification, item.code, self.player)
|
||||||
|
|
||||||
def get_filler_item_name(self) -> str:
|
def get_filler_item_name(self) -> str:
|
||||||
return self.random.choice(list(filter(lambda item: item.classification == ItemClassification.filler, itemList)))
|
return self.random.choice(list(filter(lambda item: item.classification == ItemClassification.filler, itemList))).itemName
|
||||||
|
|
||||||
def generate_output(self, output_directory: str) -> None:
|
def generate_output(self, output_directory: str) -> None:
|
||||||
patch = MLSSProcedurePatch(player=self.player, player_name=self.multiworld.player_name[self.player])
|
patch = MLSSProcedurePatch(player=self.player, player_name=self.multiworld.player_name[self.player])
|
||||||
|
|||||||
Binary file not shown.
@@ -37,7 +37,7 @@ weapons_to_name: Dict[int, str] = {
|
|||||||
minimum_weakness_requirement: Dict[int, int] = {
|
minimum_weakness_requirement: Dict[int, int] = {
|
||||||
0: 1, # Mega Buster is free
|
0: 1, # Mega Buster is free
|
||||||
1: 14, # 2 shots of Atomic Fire
|
1: 14, # 2 shots of Atomic Fire
|
||||||
2: 1, # 14 shots of Air Shooter, although you likely hit more than one shot
|
2: 2, # 14 shots of Air Shooter
|
||||||
3: 4, # 9 uses of Leaf Shield, 3 ends up 1 damage off
|
3: 4, # 9 uses of Leaf Shield, 3 ends up 1 damage off
|
||||||
4: 1, # 56 uses of Bubble Lead
|
4: 1, # 56 uses of Bubble Lead
|
||||||
5: 1, # 224 uses of Quick Boomerang
|
5: 1, # 224 uses of Quick Boomerang
|
||||||
|
|||||||
@@ -97,6 +97,28 @@ class MMBN3World(World):
|
|||||||
add_item_rule(loc, lambda item: not item.advancement)
|
add_item_rule(loc, lambda item: not item.advancement)
|
||||||
region.locations.append(loc)
|
region.locations.append(loc)
|
||||||
self.multiworld.regions.append(region)
|
self.multiworld.regions.append(region)
|
||||||
|
|
||||||
|
# Regions which contribute to explore score when accessible.
|
||||||
|
explore_score_region_names = (
|
||||||
|
RegionName.WWW_Island,
|
||||||
|
RegionName.SciLab_Overworld,
|
||||||
|
RegionName.SciLab_Cyberworld,
|
||||||
|
RegionName.Yoka_Overworld,
|
||||||
|
RegionName.Yoka_Cyberworld,
|
||||||
|
RegionName.Beach_Overworld,
|
||||||
|
RegionName.Beach_Cyberworld,
|
||||||
|
RegionName.Undernet,
|
||||||
|
RegionName.Deep_Undernet,
|
||||||
|
RegionName.Secret_Area,
|
||||||
|
)
|
||||||
|
explore_score_regions = [self.get_region(region_name) for region_name in explore_score_region_names]
|
||||||
|
|
||||||
|
# Entrances which use explore score in their logic need to register all the explore score regions as indirect
|
||||||
|
# conditions.
|
||||||
|
def register_explore_score_indirect_conditions(entrance):
|
||||||
|
for explore_score_region in explore_score_regions:
|
||||||
|
self.multiworld.register_indirect_condition(explore_score_region, entrance)
|
||||||
|
|
||||||
for region_info in regions:
|
for region_info in regions:
|
||||||
region = name_to_region[region_info.name]
|
region = name_to_region[region_info.name]
|
||||||
for connection in region_info.connections:
|
for connection in region_info.connections:
|
||||||
@@ -119,6 +141,7 @@ class MMBN3World(World):
|
|||||||
entrance.access_rule = lambda state: \
|
entrance.access_rule = lambda state: \
|
||||||
state.has(ItemName.CSciPas, self.player) or \
|
state.has(ItemName.CSciPas, self.player) or \
|
||||||
state.can_reach(RegionName.SciLab_Overworld, "Region", self.player)
|
state.can_reach(RegionName.SciLab_Overworld, "Region", self.player)
|
||||||
|
self.multiworld.register_indirect_condition(self.get_region(RegionName.SciLab_Overworld), entrance)
|
||||||
if connection == RegionName.Yoka_Cyberworld:
|
if connection == RegionName.Yoka_Cyberworld:
|
||||||
entrance.access_rule = lambda state: \
|
entrance.access_rule = lambda state: \
|
||||||
state.has(ItemName.CYokaPas, self.player) or \
|
state.has(ItemName.CYokaPas, self.player) or \
|
||||||
@@ -126,16 +149,19 @@ class MMBN3World(World):
|
|||||||
state.can_reach(RegionName.SciLab_Overworld, "Region", self.player) and
|
state.can_reach(RegionName.SciLab_Overworld, "Region", self.player) and
|
||||||
state.has(ItemName.Press, self.player)
|
state.has(ItemName.Press, self.player)
|
||||||
)
|
)
|
||||||
|
self.multiworld.register_indirect_condition(self.get_region(RegionName.SciLab_Overworld), entrance)
|
||||||
if connection == RegionName.Beach_Cyberworld:
|
if connection == RegionName.Beach_Cyberworld:
|
||||||
entrance.access_rule = lambda state: state.has(ItemName.CBeacPas, self.player) and\
|
entrance.access_rule = lambda state: state.has(ItemName.CBeacPas, self.player) and\
|
||||||
state.can_reach(RegionName.Yoka_Overworld, "Region", self.player)
|
state.can_reach(RegionName.Yoka_Overworld, "Region", self.player)
|
||||||
|
self.multiworld.register_indirect_condition(self.get_region(RegionName.Yoka_Overworld), entrance)
|
||||||
if connection == RegionName.Undernet:
|
if connection == RegionName.Undernet:
|
||||||
entrance.access_rule = lambda state: self.explore_score(state) > 8 and\
|
entrance.access_rule = lambda state: self.explore_score(state) > 8 and\
|
||||||
state.has(ItemName.Press, self.player)
|
state.has(ItemName.Press, self.player)
|
||||||
|
register_explore_score_indirect_conditions(entrance)
|
||||||
if connection == RegionName.Secret_Area:
|
if connection == RegionName.Secret_Area:
|
||||||
entrance.access_rule = lambda state: self.explore_score(state) > 12 and\
|
entrance.access_rule = lambda state: self.explore_score(state) > 12 and\
|
||||||
state.has(ItemName.Hammer, self.player)
|
state.has(ItemName.Hammer, self.player)
|
||||||
|
register_explore_score_indirect_conditions(entrance)
|
||||||
if connection == RegionName.WWW_Island:
|
if connection == RegionName.WWW_Island:
|
||||||
entrance.access_rule = lambda state:\
|
entrance.access_rule = lambda state:\
|
||||||
state.has(ItemName.Progressive_Undernet_Rank, self.player, 8)
|
state.has(ItemName.Progressive_Undernet_Rank, self.player, 8)
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
from .Utils import data_path, __version__
|
from .Utils import data_path, __version__
|
||||||
from .Colors import *
|
from .Colors import *
|
||||||
import logging
|
import logging
|
||||||
import worlds.oot.Music as music
|
from . import Music as music
|
||||||
import worlds.oot.Sounds as sfx
|
from . import Sounds as sfx
|
||||||
import worlds.oot.IconManip as icon
|
from . import IconManip as icon
|
||||||
from .JSONDump import dump_obj, CollapseList, CollapseDict, AlignedDict, SortedDict
|
from .JSONDump import dump_obj, CollapseList, CollapseDict, AlignedDict, SortedDict
|
||||||
import json
|
import json
|
||||||
|
|
||||||
@@ -105,7 +105,7 @@ def patch_tunic_colors(rom, ootworld, symbols):
|
|||||||
|
|
||||||
# handle random
|
# handle random
|
||||||
if tunic_option == 'Random Choice':
|
if tunic_option == 'Random Choice':
|
||||||
tunic_option = random.choice(tunic_color_list)
|
tunic_option = ootworld.random.choice(tunic_color_list)
|
||||||
# handle completely random
|
# handle completely random
|
||||||
if tunic_option == 'Completely Random':
|
if tunic_option == 'Completely Random':
|
||||||
color = generate_random_color()
|
color = generate_random_color()
|
||||||
@@ -156,9 +156,9 @@ def patch_navi_colors(rom, ootworld, symbols):
|
|||||||
|
|
||||||
# choose a random choice for the whole group
|
# choose a random choice for the whole group
|
||||||
if navi_option_inner == 'Random Choice':
|
if navi_option_inner == 'Random Choice':
|
||||||
navi_option_inner = random.choice(navi_color_list)
|
navi_option_inner = ootworld.random.choice(navi_color_list)
|
||||||
if navi_option_outer == 'Random Choice':
|
if navi_option_outer == 'Random Choice':
|
||||||
navi_option_outer = random.choice(navi_color_list)
|
navi_option_outer = ootworld.random.choice(navi_color_list)
|
||||||
|
|
||||||
if navi_option_outer == 'Match Inner':
|
if navi_option_outer == 'Match Inner':
|
||||||
navi_option_outer = navi_option_inner
|
navi_option_outer = navi_option_inner
|
||||||
@@ -233,9 +233,9 @@ def patch_sword_trails(rom, ootworld, symbols):
|
|||||||
|
|
||||||
# handle random choice
|
# handle random choice
|
||||||
if option_inner == 'Random Choice':
|
if option_inner == 'Random Choice':
|
||||||
option_inner = random.choice(sword_trail_color_list)
|
option_inner = ootworld.random.choice(sword_trail_color_list)
|
||||||
if option_outer == 'Random Choice':
|
if option_outer == 'Random Choice':
|
||||||
option_outer = random.choice(sword_trail_color_list)
|
option_outer = ootworld.random.choice(sword_trail_color_list)
|
||||||
|
|
||||||
if option_outer == 'Match Inner':
|
if option_outer == 'Match Inner':
|
||||||
option_outer = option_inner
|
option_outer = option_inner
|
||||||
@@ -326,9 +326,9 @@ def patch_trails(rom, ootworld, trails):
|
|||||||
|
|
||||||
# handle random choice
|
# handle random choice
|
||||||
if option_inner == 'Random Choice':
|
if option_inner == 'Random Choice':
|
||||||
option_inner = random.choice(trail_color_list)
|
option_inner = ootworld.random.choice(trail_color_list)
|
||||||
if option_outer == 'Random Choice':
|
if option_outer == 'Random Choice':
|
||||||
option_outer = random.choice(trail_color_list)
|
option_outer = ootworld.random.choice(trail_color_list)
|
||||||
|
|
||||||
if option_outer == 'Match Inner':
|
if option_outer == 'Match Inner':
|
||||||
option_outer = option_inner
|
option_outer = option_inner
|
||||||
@@ -393,7 +393,7 @@ def patch_gauntlet_colors(rom, ootworld, symbols):
|
|||||||
|
|
||||||
# handle random
|
# handle random
|
||||||
if gauntlet_option == 'Random Choice':
|
if gauntlet_option == 'Random Choice':
|
||||||
gauntlet_option = random.choice(gauntlet_color_list)
|
gauntlet_option = ootworld.random.choice(gauntlet_color_list)
|
||||||
# handle completely random
|
# handle completely random
|
||||||
if gauntlet_option == 'Completely Random':
|
if gauntlet_option == 'Completely Random':
|
||||||
color = generate_random_color()
|
color = generate_random_color()
|
||||||
@@ -424,10 +424,10 @@ def patch_shield_frame_colors(rom, ootworld, symbols):
|
|||||||
|
|
||||||
# handle random
|
# handle random
|
||||||
if shield_frame_option == 'Random Choice':
|
if shield_frame_option == 'Random Choice':
|
||||||
shield_frame_option = random.choice(shield_frame_color_list)
|
shield_frame_option = ootworld.random.choice(shield_frame_color_list)
|
||||||
# handle completely random
|
# handle completely random
|
||||||
if shield_frame_option == 'Completely Random':
|
if shield_frame_option == 'Completely Random':
|
||||||
color = [random.getrandbits(8), random.getrandbits(8), random.getrandbits(8)]
|
color = [ootworld.random.getrandbits(8), ootworld.random.getrandbits(8), ootworld.random.getrandbits(8)]
|
||||||
# grab the color from the list
|
# grab the color from the list
|
||||||
elif shield_frame_option in shield_frame_colors:
|
elif shield_frame_option in shield_frame_colors:
|
||||||
color = list(shield_frame_colors[shield_frame_option])
|
color = list(shield_frame_colors[shield_frame_option])
|
||||||
@@ -458,7 +458,7 @@ def patch_heart_colors(rom, ootworld, symbols):
|
|||||||
|
|
||||||
# handle random
|
# handle random
|
||||||
if heart_option == 'Random Choice':
|
if heart_option == 'Random Choice':
|
||||||
heart_option = random.choice(heart_color_list)
|
heart_option = ootworld.random.choice(heart_color_list)
|
||||||
# handle completely random
|
# handle completely random
|
||||||
if heart_option == 'Completely Random':
|
if heart_option == 'Completely Random':
|
||||||
color = generate_random_color()
|
color = generate_random_color()
|
||||||
@@ -495,7 +495,7 @@ def patch_magic_colors(rom, ootworld, symbols):
|
|||||||
magic_option = format_cosmetic_option_result(ootworld.__dict__[magic_setting])
|
magic_option = format_cosmetic_option_result(ootworld.__dict__[magic_setting])
|
||||||
|
|
||||||
if magic_option == 'Random Choice':
|
if magic_option == 'Random Choice':
|
||||||
magic_option = random.choice(magic_color_list)
|
magic_option = ootworld.random.choice(magic_color_list)
|
||||||
|
|
||||||
if magic_option == 'Completely Random':
|
if magic_option == 'Completely Random':
|
||||||
color = generate_random_color()
|
color = generate_random_color()
|
||||||
@@ -559,7 +559,7 @@ def patch_button_colors(rom, ootworld, symbols):
|
|||||||
|
|
||||||
# handle random
|
# handle random
|
||||||
if button_option == 'Random Choice':
|
if button_option == 'Random Choice':
|
||||||
button_option = random.choice(list(button_colors.keys()))
|
button_option = ootworld.random.choice(list(button_colors.keys()))
|
||||||
# handle completely random
|
# handle completely random
|
||||||
if button_option == 'Completely Random':
|
if button_option == 'Completely Random':
|
||||||
fixed_font_color = [10, 10, 10]
|
fixed_font_color = [10, 10, 10]
|
||||||
@@ -618,11 +618,11 @@ def patch_sfx(rom, ootworld, symbols):
|
|||||||
rom.write_int16(loc, sound_id)
|
rom.write_int16(loc, sound_id)
|
||||||
else:
|
else:
|
||||||
if selection == 'random-choice':
|
if selection == 'random-choice':
|
||||||
selection = random.choice(sfx.get_hook_pool(hook)).value.keyword
|
selection = ootworld.random.choice(sfx.get_hook_pool(hook)).value.keyword
|
||||||
elif selection == 'random-ear-safe':
|
elif selection == 'random-ear-safe':
|
||||||
selection = random.choice(sfx.get_hook_pool(hook, "TRUE")).value.keyword
|
selection = ootworld.random.choice(sfx.get_hook_pool(hook, "TRUE")).value.keyword
|
||||||
elif selection == 'completely-random':
|
elif selection == 'completely-random':
|
||||||
selection = random.choice(sfx.standard).value.keyword
|
selection = ootworld.random.choice(sfx.standard).value.keyword
|
||||||
sound_id = sound_dict[selection]
|
sound_id = sound_dict[selection]
|
||||||
for loc in hook.value.locations:
|
for loc in hook.value.locations:
|
||||||
rom.write_int16(loc, sound_id)
|
rom.write_int16(loc, sound_id)
|
||||||
@@ -644,7 +644,7 @@ def patch_instrument(rom, ootworld, symbols):
|
|||||||
|
|
||||||
choice = ootworld.sfx_ocarina
|
choice = ootworld.sfx_ocarina
|
||||||
if choice == 'random-choice':
|
if choice == 'random-choice':
|
||||||
choice = random.choice(list(instruments.keys()))
|
choice = ootworld.random.choice(list(instruments.keys()))
|
||||||
|
|
||||||
rom.write_byte(0x00B53C7B, instruments[choice])
|
rom.write_byte(0x00B53C7B, instruments[choice])
|
||||||
rom.write_byte(0x00B4BF6F, instruments[choice]) # For Lost Woods Skull Kids' minigame in Lost Woods
|
rom.write_byte(0x00B4BF6F, instruments[choice]) # For Lost Woods Skull Kids' minigame in Lost Woods
|
||||||
@@ -769,7 +769,6 @@ patch_sets[0x1F073FD9] = {
|
|||||||
|
|
||||||
def patch_cosmetics(ootworld, rom):
|
def patch_cosmetics(ootworld, rom):
|
||||||
# Use the world's slot seed for cosmetics
|
# Use the world's slot seed for cosmetics
|
||||||
random.seed(ootworld.multiworld.per_slot_randoms[ootworld.player].random())
|
|
||||||
|
|
||||||
# try to detect the cosmetic patch data format
|
# try to detect the cosmetic patch data format
|
||||||
versioned_patch_set = None
|
versioned_patch_set = None
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ from BaseClasses import Entrance
|
|||||||
class OOTEntrance(Entrance):
|
class OOTEntrance(Entrance):
|
||||||
game: str = 'Ocarina of Time'
|
game: str = 'Ocarina of Time'
|
||||||
|
|
||||||
def __init__(self, player, world, name='', parent=None):
|
def __init__(self, player, multiworld, name='', parent=None):
|
||||||
super(OOTEntrance, self).__init__(player, name, parent)
|
super(OOTEntrance, self).__init__(player, name, parent)
|
||||||
self.multiworld = world
|
self.multiworld = multiworld
|
||||||
self.access_rules = []
|
self.access_rules = []
|
||||||
self.reverse = None
|
self.reverse = None
|
||||||
self.replaces = None
|
self.replaces = None
|
||||||
|
|||||||
@@ -440,16 +440,16 @@ class EntranceShuffleError(Exception):
|
|||||||
|
|
||||||
|
|
||||||
def shuffle_random_entrances(ootworld):
|
def shuffle_random_entrances(ootworld):
|
||||||
world = ootworld.multiworld
|
multiworld = ootworld.multiworld
|
||||||
player = ootworld.player
|
player = ootworld.player
|
||||||
|
|
||||||
# Gather locations to keep reachable for validation
|
# Gather locations to keep reachable for validation
|
||||||
all_state = ootworld.get_state_with_complete_itempool()
|
all_state = ootworld.get_state_with_complete_itempool()
|
||||||
all_state.sweep_for_advancements(locations=ootworld.get_locations())
|
all_state.sweep_for_advancements(locations=ootworld.get_locations())
|
||||||
locations_to_ensure_reachable = {loc for loc in world.get_reachable_locations(all_state, player) if not (loc.type == 'Drop' or (loc.type == 'Event' and 'Subrule' in loc.name))}
|
locations_to_ensure_reachable = {loc for loc in multiworld.get_reachable_locations(all_state, player) if not (loc.type == 'Drop' or (loc.type == 'Event' and 'Subrule' in loc.name))}
|
||||||
|
|
||||||
# Set entrance data for all entrances
|
# Set entrance data for all entrances
|
||||||
set_all_entrances_data(world, player)
|
set_all_entrances_data(multiworld, player)
|
||||||
|
|
||||||
# Determine entrance pools based on settings
|
# Determine entrance pools based on settings
|
||||||
one_way_entrance_pools = {}
|
one_way_entrance_pools = {}
|
||||||
@@ -547,10 +547,10 @@ def shuffle_random_entrances(ootworld):
|
|||||||
none_state = CollectionState(ootworld.multiworld)
|
none_state = CollectionState(ootworld.multiworld)
|
||||||
|
|
||||||
# Plando entrances
|
# Plando entrances
|
||||||
if world.plando_connections[player]:
|
if ootworld.options.plando_connections:
|
||||||
rollbacks = []
|
rollbacks = []
|
||||||
all_targets = {**one_way_target_entrance_pools, **target_entrance_pools}
|
all_targets = {**one_way_target_entrance_pools, **target_entrance_pools}
|
||||||
for conn in world.plando_connections[player]:
|
for conn in ootworld.options.plando_connections:
|
||||||
try:
|
try:
|
||||||
entrance = ootworld.get_entrance(conn.entrance)
|
entrance = ootworld.get_entrance(conn.entrance)
|
||||||
exit = ootworld.get_entrance(conn.exit)
|
exit = ootworld.get_entrance(conn.exit)
|
||||||
@@ -628,7 +628,7 @@ def shuffle_random_entrances(ootworld):
|
|||||||
logging.getLogger('').error(f'Root has too many entrances left after shuffling entrances')
|
logging.getLogger('').error(f'Root has too many entrances left after shuffling entrances')
|
||||||
# Game is beatable
|
# Game is beatable
|
||||||
new_all_state = ootworld.get_state_with_complete_itempool()
|
new_all_state = ootworld.get_state_with_complete_itempool()
|
||||||
if not world.has_beaten_game(new_all_state, player):
|
if not multiworld.has_beaten_game(new_all_state, player):
|
||||||
raise EntranceShuffleError('Cannot beat game')
|
raise EntranceShuffleError('Cannot beat game')
|
||||||
# Validate world
|
# Validate world
|
||||||
validate_world(ootworld, None, locations_to_ensure_reachable, all_state, none_state)
|
validate_world(ootworld, None, locations_to_ensure_reachable, all_state, none_state)
|
||||||
@@ -675,7 +675,7 @@ def place_one_way_priority_entrance(ootworld, priority_name, allowed_regions, al
|
|||||||
all_state, none_state, one_way_entrance_pools, one_way_target_entrance_pools):
|
all_state, none_state, one_way_entrance_pools, one_way_target_entrance_pools):
|
||||||
|
|
||||||
avail_pool = list(chain.from_iterable(one_way_entrance_pools[t] for t in allowed_types if t in one_way_entrance_pools))
|
avail_pool = list(chain.from_iterable(one_way_entrance_pools[t] for t in allowed_types if t in one_way_entrance_pools))
|
||||||
ootworld.multiworld.random.shuffle(avail_pool)
|
ootworld.random.shuffle(avail_pool)
|
||||||
|
|
||||||
for entrance in avail_pool:
|
for entrance in avail_pool:
|
||||||
if entrance.replaces:
|
if entrance.replaces:
|
||||||
@@ -725,11 +725,11 @@ def shuffle_entrance_pool(ootworld, pool_type, entrance_pool, target_entrances,
|
|||||||
raise EntranceShuffleError(f'Entrance placement attempt count exceeded for world {ootworld.player}')
|
raise EntranceShuffleError(f'Entrance placement attempt count exceeded for world {ootworld.player}')
|
||||||
|
|
||||||
def shuffle_entrances(ootworld, pool_type, entrances, target_entrances, rollbacks, locations_to_ensure_reachable, all_state, none_state):
|
def shuffle_entrances(ootworld, pool_type, entrances, target_entrances, rollbacks, locations_to_ensure_reachable, all_state, none_state):
|
||||||
ootworld.multiworld.random.shuffle(entrances)
|
ootworld.random.shuffle(entrances)
|
||||||
for entrance in entrances:
|
for entrance in entrances:
|
||||||
if entrance.connected_region != None:
|
if entrance.connected_region != None:
|
||||||
continue
|
continue
|
||||||
ootworld.multiworld.random.shuffle(target_entrances)
|
ootworld.random.shuffle(target_entrances)
|
||||||
# Here we deliberately introduce bias by prioritizing certain interiors, i.e. the ones most likely to cause problems.
|
# Here we deliberately introduce bias by prioritizing certain interiors, i.e. the ones most likely to cause problems.
|
||||||
# success rate over randomization
|
# success rate over randomization
|
||||||
if pool_type in {'InteriorSoft', 'MixedSoft'}:
|
if pool_type in {'InteriorSoft', 'MixedSoft'}:
|
||||||
@@ -785,7 +785,7 @@ def split_entrances_by_requirements(ootworld, entrances_to_split, assumed_entran
|
|||||||
# TODO: improve this function
|
# TODO: improve this function
|
||||||
def validate_world(ootworld, entrance_placed, locations_to_ensure_reachable, all_state_orig, none_state_orig):
|
def validate_world(ootworld, entrance_placed, locations_to_ensure_reachable, all_state_orig, none_state_orig):
|
||||||
|
|
||||||
world = ootworld.multiworld
|
multiworld = ootworld.multiworld
|
||||||
player = ootworld.player
|
player = ootworld.player
|
||||||
|
|
||||||
all_state = all_state_orig.copy()
|
all_state = all_state_orig.copy()
|
||||||
@@ -828,8 +828,8 @@ def validate_world(ootworld, entrance_placed, locations_to_ensure_reachable, all
|
|||||||
if ootworld.shuffle_interior_entrances and (ootworld.misc_hints or ootworld.hints != 'none') and \
|
if ootworld.shuffle_interior_entrances and (ootworld.misc_hints or ootworld.hints != 'none') and \
|
||||||
(entrance_placed == None or entrance_placed.type in ['Interior', 'SpecialInterior']):
|
(entrance_placed == None or entrance_placed.type in ['Interior', 'SpecialInterior']):
|
||||||
# Ensure Kak Potion Shop entrances are in the same hint area so there is no ambiguity as to which entrance is used for hints
|
# Ensure Kak Potion Shop entrances are in the same hint area so there is no ambiguity as to which entrance is used for hints
|
||||||
potion_front = get_entrance_replacing(world.get_region('Kak Potion Shop Front', player), 'Kakariko Village -> Kak Potion Shop Front', player)
|
potion_front = get_entrance_replacing(multiworld.get_region('Kak Potion Shop Front', player), 'Kakariko Village -> Kak Potion Shop Front', player)
|
||||||
potion_back = get_entrance_replacing(world.get_region('Kak Potion Shop Back', player), 'Kak Backyard -> Kak Potion Shop Back', player)
|
potion_back = get_entrance_replacing(multiworld.get_region('Kak Potion Shop Back', player), 'Kak Backyard -> Kak Potion Shop Back', player)
|
||||||
if potion_front is not None and potion_back is not None and not same_hint_area(potion_front, potion_back):
|
if potion_front is not None and potion_back is not None and not same_hint_area(potion_front, potion_back):
|
||||||
raise EntranceShuffleError('Kak Potion Shop entrances are not in the same hint area')
|
raise EntranceShuffleError('Kak Potion Shop entrances are not in the same hint area')
|
||||||
elif (potion_front and not potion_back) or (not potion_front and potion_back):
|
elif (potion_front and not potion_back) or (not potion_front and potion_back):
|
||||||
@@ -840,8 +840,8 @@ def validate_world(ootworld, entrance_placed, locations_to_ensure_reachable, all
|
|||||||
|
|
||||||
# When cows are shuffled, ensure the same thing for Impa's House, since the cow is reachable from both sides
|
# When cows are shuffled, ensure the same thing for Impa's House, since the cow is reachable from both sides
|
||||||
if ootworld.shuffle_cows:
|
if ootworld.shuffle_cows:
|
||||||
impas_front = get_entrance_replacing(world.get_region('Kak Impas House', player), 'Kakariko Village -> Kak Impas House', player)
|
impas_front = get_entrance_replacing(multiworld.get_region('Kak Impas House', player), 'Kakariko Village -> Kak Impas House', player)
|
||||||
impas_back = get_entrance_replacing(world.get_region('Kak Impas House Back', player), 'Kak Impas Ledge -> Kak Impas House Back', player)
|
impas_back = get_entrance_replacing(multiworld.get_region('Kak Impas House Back', player), 'Kak Impas Ledge -> Kak Impas House Back', player)
|
||||||
if impas_front is not None and impas_back is not None and not same_hint_area(impas_front, impas_back):
|
if impas_front is not None and impas_back is not None and not same_hint_area(impas_front, impas_back):
|
||||||
raise EntranceShuffleError('Kak Impas House entrances are not in the same hint area')
|
raise EntranceShuffleError('Kak Impas House entrances are not in the same hint area')
|
||||||
elif (impas_front and not impas_back) or (not impas_front and impas_back):
|
elif (impas_front and not impas_back) or (not impas_front and impas_back):
|
||||||
@@ -861,25 +861,25 @@ def validate_world(ootworld, entrance_placed, locations_to_ensure_reachable, all
|
|||||||
any(region for region in time_travel_state.adult_reachable_regions[player] if region.time_passes)):
|
any(region for region in time_travel_state.adult_reachable_regions[player] if region.time_passes)):
|
||||||
raise EntranceShuffleError('Time passing is not guaranteed as both ages')
|
raise EntranceShuffleError('Time passing is not guaranteed as both ages')
|
||||||
|
|
||||||
if ootworld.starting_age == 'child' and (world.get_region('Temple of Time', player) not in time_travel_state.adult_reachable_regions[player]):
|
if ootworld.starting_age == 'child' and (multiworld.get_region('Temple of Time', player) not in time_travel_state.adult_reachable_regions[player]):
|
||||||
raise EntranceShuffleError('Path to ToT as adult not guaranteed')
|
raise EntranceShuffleError('Path to ToT as adult not guaranteed')
|
||||||
if ootworld.starting_age == 'adult' and (world.get_region('Temple of Time', player) not in time_travel_state.child_reachable_regions[player]):
|
if ootworld.starting_age == 'adult' and (multiworld.get_region('Temple of Time', player) not in time_travel_state.child_reachable_regions[player]):
|
||||||
raise EntranceShuffleError('Path to ToT as child not guaranteed')
|
raise EntranceShuffleError('Path to ToT as child not guaranteed')
|
||||||
|
|
||||||
if (ootworld.shuffle_interior_entrances or ootworld.shuffle_overworld_entrances) and \
|
if (ootworld.shuffle_interior_entrances or ootworld.shuffle_overworld_entrances) and \
|
||||||
(entrance_placed == None or entrance_placed.type in ['Interior', 'SpecialInterior', 'Overworld', 'Spawn', 'WarpSong', 'OwlDrop']):
|
(entrance_placed == None or entrance_placed.type in ['Interior', 'SpecialInterior', 'Overworld', 'Spawn', 'WarpSong', 'OwlDrop']):
|
||||||
# Ensure big poe shop is always reachable as adult
|
# Ensure big poe shop is always reachable as adult
|
||||||
if world.get_region('Market Guard House', player) not in time_travel_state.adult_reachable_regions[player]:
|
if multiworld.get_region('Market Guard House', player) not in time_travel_state.adult_reachable_regions[player]:
|
||||||
raise EntranceShuffleError('Big Poe Shop access not guaranteed as adult')
|
raise EntranceShuffleError('Big Poe Shop access not guaranteed as adult')
|
||||||
if ootworld.shopsanity == 'off':
|
if ootworld.shopsanity == 'off':
|
||||||
# Ensure that Goron and Zora shops are accessible as adult
|
# Ensure that Goron and Zora shops are accessible as adult
|
||||||
if world.get_region('GC Shop', player) not in all_state.adult_reachable_regions[player]:
|
if multiworld.get_region('GC Shop', player) not in all_state.adult_reachable_regions[player]:
|
||||||
raise EntranceShuffleError('Goron City Shop not accessible as adult')
|
raise EntranceShuffleError('Goron City Shop not accessible as adult')
|
||||||
if world.get_region('ZD Shop', player) not in all_state.adult_reachable_regions[player]:
|
if multiworld.get_region('ZD Shop', player) not in all_state.adult_reachable_regions[player]:
|
||||||
raise EntranceShuffleError('Zora\'s Domain Shop not accessible as adult')
|
raise EntranceShuffleError('Zora\'s Domain Shop not accessible as adult')
|
||||||
if ootworld.open_forest == 'closed':
|
if ootworld.open_forest == 'closed':
|
||||||
# Ensure that Kokiri Shop is reachable as child with no items
|
# Ensure that Kokiri Shop is reachable as child with no items
|
||||||
if world.get_region('KF Kokiri Shop', player) not in none_state.child_reachable_regions[player]:
|
if multiworld.get_region('KF Kokiri Shop', player) not in none_state.child_reachable_regions[player]:
|
||||||
raise EntranceShuffleError('Kokiri Forest Shop not accessible as child in closed forest')
|
raise EntranceShuffleError('Kokiri Forest Shop not accessible as child in closed forest')
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import random
|
|
||||||
|
|
||||||
from BaseClasses import LocationProgressType
|
from BaseClasses import LocationProgressType
|
||||||
from .Items import OOTItem
|
from .Items import OOTItem
|
||||||
|
|
||||||
@@ -28,7 +26,7 @@ class Hint(object):
|
|||||||
text = ""
|
text = ""
|
||||||
type = []
|
type = []
|
||||||
|
|
||||||
def __init__(self, name, text, type, choice=None):
|
def __init__(self, name, text, type, rand, choice=None):
|
||||||
self.name = name
|
self.name = name
|
||||||
self.type = [type] if not isinstance(type, list) else type
|
self.type = [type] if not isinstance(type, list) else type
|
||||||
|
|
||||||
@@ -36,31 +34,31 @@ class Hint(object):
|
|||||||
self.text = text
|
self.text = text
|
||||||
else:
|
else:
|
||||||
if choice == None:
|
if choice == None:
|
||||||
self.text = random.choice(text)
|
self.text = rand.choice(text)
|
||||||
else:
|
else:
|
||||||
self.text = text[choice]
|
self.text = text[choice]
|
||||||
|
|
||||||
|
|
||||||
def getHint(item, clearer_hint=False):
|
def getHint(item, rand, clearer_hint=False):
|
||||||
if item in hintTable:
|
if item in hintTable:
|
||||||
textOptions, clearText, hintType = hintTable[item]
|
textOptions, clearText, hintType = hintTable[item]
|
||||||
if clearer_hint:
|
if clearer_hint:
|
||||||
if clearText == None:
|
if clearText == None:
|
||||||
return Hint(item, textOptions, hintType, 0)
|
return Hint(item, textOptions, hintType, rand, 0)
|
||||||
return Hint(item, clearText, hintType)
|
return Hint(item, clearText, hintType, rand)
|
||||||
else:
|
else:
|
||||||
return Hint(item, textOptions, hintType)
|
return Hint(item, textOptions, hintType, rand)
|
||||||
elif isinstance(item, str):
|
elif isinstance(item, str):
|
||||||
return Hint(item, item, 'generic')
|
return Hint(item, item, 'generic', rand)
|
||||||
else: # is an Item
|
else: # is an Item
|
||||||
return Hint(item.name, item.hint_text, 'item')
|
return Hint(item.name, item.hint_text, 'item', rand)
|
||||||
|
|
||||||
|
|
||||||
def getHintGroup(group, world):
|
def getHintGroup(group, world):
|
||||||
ret = []
|
ret = []
|
||||||
for name in hintTable:
|
for name in hintTable:
|
||||||
|
|
||||||
hint = getHint(name, world.clearer_hints)
|
hint = getHint(name, world.random, world.clearer_hints)
|
||||||
|
|
||||||
if hint.name in world.always_hints and group == 'always':
|
if hint.name in world.always_hints and group == 'always':
|
||||||
hint.type = 'always'
|
hint.type = 'always'
|
||||||
@@ -95,7 +93,7 @@ def getHintGroup(group, world):
|
|||||||
def getRequiredHints(world):
|
def getRequiredHints(world):
|
||||||
ret = []
|
ret = []
|
||||||
for name in hintTable:
|
for name in hintTable:
|
||||||
hint = getHint(name)
|
hint = getHint(name, world.random)
|
||||||
if 'always' in hint.type or hint.name in conditional_always and conditional_always[hint.name](world):
|
if 'always' in hint.type or hint.name in conditional_always and conditional_always[hint.name](world):
|
||||||
ret.append(hint)
|
ret.append(hint)
|
||||||
return ret
|
return ret
|
||||||
@@ -1689,7 +1687,7 @@ def hintExclusions(world, clear_cache=False):
|
|||||||
|
|
||||||
location_hints = []
|
location_hints = []
|
||||||
for name in hintTable:
|
for name in hintTable:
|
||||||
hint = getHint(name, world.clearer_hints)
|
hint = getHint(name, world.random, world.clearer_hints)
|
||||||
if any(item in hint.type for item in
|
if any(item in hint.type for item in
|
||||||
['always',
|
['always',
|
||||||
'dual_always',
|
'dual_always',
|
||||||
|
|||||||
@@ -136,13 +136,13 @@ def getItemGenericName(item):
|
|||||||
def isRestrictedDungeonItem(dungeon, item):
|
def isRestrictedDungeonItem(dungeon, item):
|
||||||
if not isinstance(item, OOTItem):
|
if not isinstance(item, OOTItem):
|
||||||
return False
|
return False
|
||||||
if (item.map or item.compass) and dungeon.multiworld.shuffle_mapcompass == 'dungeon':
|
if (item.map or item.compass) and dungeon.world.options.shuffle_mapcompass == 'dungeon':
|
||||||
return item in dungeon.dungeon_items
|
return item in dungeon.dungeon_items
|
||||||
if item.type == 'SmallKey' and dungeon.multiworld.shuffle_smallkeys == 'dungeon':
|
if item.type == 'SmallKey' and dungeon.world.options.shuffle_smallkeys == 'dungeon':
|
||||||
return item in dungeon.small_keys
|
return item in dungeon.small_keys
|
||||||
if item.type == 'BossKey' and dungeon.multiworld.shuffle_bosskeys == 'dungeon':
|
if item.type == 'BossKey' and dungeon.world.options.shuffle_bosskeys == 'dungeon':
|
||||||
return item in dungeon.boss_key
|
return item in dungeon.boss_key
|
||||||
if item.type == 'GanonBossKey' and dungeon.multiworld.shuffle_ganon_bosskey == 'dungeon':
|
if item.type == 'GanonBossKey' and dungeon.world.options.shuffle_ganon_bosskey == 'dungeon':
|
||||||
return item in dungeon.boss_key
|
return item in dungeon.boss_key
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -261,8 +261,8 @@ hintPrefixes = [
|
|||||||
'',
|
'',
|
||||||
]
|
]
|
||||||
|
|
||||||
def getSimpleHintNoPrefix(item):
|
def getSimpleHintNoPrefix(item, rand):
|
||||||
hint = getHint(item.name, True).text
|
hint = getHint(item.name, rand, True).text
|
||||||
|
|
||||||
for prefix in hintPrefixes:
|
for prefix in hintPrefixes:
|
||||||
if hint.startswith(prefix):
|
if hint.startswith(prefix):
|
||||||
@@ -417,9 +417,9 @@ class HintArea(Enum):
|
|||||||
|
|
||||||
# Formats the hint text for this area with proper grammar.
|
# Formats the hint text for this area with proper grammar.
|
||||||
# Dungeons are hinted differently depending on the clearer_hints setting.
|
# Dungeons are hinted differently depending on the clearer_hints setting.
|
||||||
def text(self, clearer_hints, preposition=False, world=None):
|
def text(self, rand, clearer_hints, preposition=False, world=None):
|
||||||
if self.is_dungeon:
|
if self.is_dungeon:
|
||||||
text = getHint(self.dungeon_name, clearer_hints).text
|
text = getHint(self.dungeon_name, rand, clearer_hints).text
|
||||||
else:
|
else:
|
||||||
text = str(self)
|
text = str(self)
|
||||||
prefix, suffix = text.replace('#', '').split(' ', 1)
|
prefix, suffix = text.replace('#', '').split(' ', 1)
|
||||||
@@ -489,7 +489,7 @@ def get_woth_hint(world, checked):
|
|||||||
|
|
||||||
if getattr(location.parent_region, "dungeon", None):
|
if getattr(location.parent_region, "dungeon", None):
|
||||||
world.woth_dungeon += 1
|
world.woth_dungeon += 1
|
||||||
location_text = getHint(location.parent_region.dungeon.name, world.clearer_hints).text
|
location_text = getHint(location.parent_region.dungeon.name, world.random, world.clearer_hints).text
|
||||||
else:
|
else:
|
||||||
location_text = get_hint_area(location)
|
location_text = get_hint_area(location)
|
||||||
|
|
||||||
@@ -570,9 +570,9 @@ def get_good_item_hint(world, checked):
|
|||||||
location = world.hint_rng.choice(locations)
|
location = world.hint_rng.choice(locations)
|
||||||
checked[location.player].add(location.name)
|
checked[location.player].add(location.name)
|
||||||
|
|
||||||
item_text = getHint(getItemGenericName(location.item), world.clearer_hints).text
|
item_text = getHint(getItemGenericName(location.item), world.hint_rng, world.clearer_hints).text
|
||||||
if getattr(location.parent_region, "dungeon", None):
|
if getattr(location.parent_region, "dungeon", None):
|
||||||
location_text = getHint(location.parent_region.dungeon.name, world.clearer_hints).text
|
location_text = getHint(location.parent_region.dungeon.name, world.hint_rng, world.clearer_hints).text
|
||||||
return (GossipText('#%s# hoards #%s#.' % (attach_name(location_text, location, world), attach_name(item_text, location.item, world)),
|
return (GossipText('#%s# hoards #%s#.' % (attach_name(location_text, location, world), attach_name(item_text, location.item, world)),
|
||||||
['Green', 'Red']), location)
|
['Green', 'Red']), location)
|
||||||
else:
|
else:
|
||||||
@@ -613,10 +613,10 @@ def get_specific_item_hint(world, checked):
|
|||||||
|
|
||||||
location = world.hint_rng.choice(locations)
|
location = world.hint_rng.choice(locations)
|
||||||
checked[location.player].add(location.name)
|
checked[location.player].add(location.name)
|
||||||
item_text = getHint(getItemGenericName(location.item), world.clearer_hints).text
|
item_text = getHint(getItemGenericName(location.item), world.hint_rng, world.clearer_hints).text
|
||||||
|
|
||||||
if getattr(location.parent_region, "dungeon", None):
|
if getattr(location.parent_region, "dungeon", None):
|
||||||
location_text = getHint(location.parent_region.dungeon.name, world.clearer_hints).text
|
location_text = getHint(location.parent_region.dungeon.name, world.hint_rng, world.clearer_hints).text
|
||||||
if world.hint_dist_user.get('vague_named_items', False):
|
if world.hint_dist_user.get('vague_named_items', False):
|
||||||
return (GossipText('#%s# may be on the hero\'s path.' % (location_text), ['Green']), location)
|
return (GossipText('#%s# may be on the hero\'s path.' % (location_text), ['Green']), location)
|
||||||
else:
|
else:
|
||||||
@@ -648,9 +648,9 @@ def get_random_location_hint(world, checked):
|
|||||||
checked[location.player].add(location.name)
|
checked[location.player].add(location.name)
|
||||||
dungeon = location.parent_region.dungeon
|
dungeon = location.parent_region.dungeon
|
||||||
|
|
||||||
item_text = getHint(getItemGenericName(location.item), world.clearer_hints).text
|
item_text = getHint(getItemGenericName(location.item), world.hint_rng, world.clearer_hints).text
|
||||||
if dungeon:
|
if dungeon:
|
||||||
location_text = getHint(dungeon.name, world.clearer_hints).text
|
location_text = getHint(dungeon.name, world.hint_rng, world.clearer_hints).text
|
||||||
return (GossipText('#%s# hoards #%s#.' % (attach_name(location_text, location, world), attach_name(item_text, location.item, world)),
|
return (GossipText('#%s# hoards #%s#.' % (attach_name(location_text, location, world), attach_name(item_text, location.item, world)),
|
||||||
['Green', 'Red']), location)
|
['Green', 'Red']), location)
|
||||||
else:
|
else:
|
||||||
@@ -675,7 +675,7 @@ def get_specific_hint(world, checked, type):
|
|||||||
location_text = hint.text
|
location_text = hint.text
|
||||||
if '#' not in location_text:
|
if '#' not in location_text:
|
||||||
location_text = '#%s#' % location_text
|
location_text = '#%s#' % location_text
|
||||||
item_text = getHint(getItemGenericName(location.item), world.clearer_hints).text
|
item_text = getHint(getItemGenericName(location.item), world.hint_rng, world.clearer_hints).text
|
||||||
|
|
||||||
return (GossipText('%s #%s#.' % (attach_name(location_text, location, world), attach_name(item_text, location.item, world)),
|
return (GossipText('%s #%s#.' % (attach_name(location_text, location, world), attach_name(item_text, location.item, world)),
|
||||||
['Green', 'Red']), location)
|
['Green', 'Red']), location)
|
||||||
@@ -724,9 +724,9 @@ def get_entrance_hint(world, checked):
|
|||||||
|
|
||||||
connected_region = entrance.connected_region
|
connected_region = entrance.connected_region
|
||||||
if connected_region.dungeon:
|
if connected_region.dungeon:
|
||||||
region_text = getHint(connected_region.dungeon.name, world.clearer_hints).text
|
region_text = getHint(connected_region.dungeon.name, world.hint_rng, world.clearer_hints).text
|
||||||
else:
|
else:
|
||||||
region_text = getHint(connected_region.name, world.clearer_hints).text
|
region_text = getHint(connected_region.name, world.hint_rng, world.clearer_hints).text
|
||||||
|
|
||||||
if '#' not in region_text:
|
if '#' not in region_text:
|
||||||
region_text = '#%s#' % region_text
|
region_text = '#%s#' % region_text
|
||||||
@@ -882,10 +882,10 @@ def buildWorldGossipHints(world, checkedLocations=None):
|
|||||||
if location.name in world.hint_text_overrides:
|
if location.name in world.hint_text_overrides:
|
||||||
location_text = world.hint_text_overrides[location.name]
|
location_text = world.hint_text_overrides[location.name]
|
||||||
else:
|
else:
|
||||||
location_text = getHint(location.name, world.clearer_hints).text
|
location_text = getHint(location.name, world.hint_rng, world.clearer_hints).text
|
||||||
if '#' not in location_text:
|
if '#' not in location_text:
|
||||||
location_text = '#%s#' % location_text
|
location_text = '#%s#' % location_text
|
||||||
item_text = getHint(getItemGenericName(location.item), world.clearer_hints).text
|
item_text = getHint(getItemGenericName(location.item), world.hint_rng, world.clearer_hints).text
|
||||||
add_hint(world, stoneGroups, GossipText('%s #%s#.' % (attach_name(location_text, location, world), attach_name(item_text, location.item, world)),
|
add_hint(world, stoneGroups, GossipText('%s #%s#.' % (attach_name(location_text, location, world), attach_name(item_text, location.item, world)),
|
||||||
['Green', 'Red']), hint_dist['always'][1], location, force_reachable=True)
|
['Green', 'Red']), hint_dist['always'][1], location, force_reachable=True)
|
||||||
logging.getLogger('').debug('Placed always hint for %s.', location.name)
|
logging.getLogger('').debug('Placed always hint for %s.', location.name)
|
||||||
@@ -1003,16 +1003,16 @@ def buildAltarHints(world, messages, include_rewards=True, include_wincons=True)
|
|||||||
('Goron Ruby', 'Red'),
|
('Goron Ruby', 'Red'),
|
||||||
('Zora Sapphire', 'Blue'),
|
('Zora Sapphire', 'Blue'),
|
||||||
]
|
]
|
||||||
child_text += getHint('Spiritual Stone Text Start', world.clearer_hints).text + '\x04'
|
child_text += getHint('Spiritual Stone Text Start', world.hint_rng, world.clearer_hints).text + '\x04'
|
||||||
for (reward, color) in bossRewardsSpiritualStones:
|
for (reward, color) in bossRewardsSpiritualStones:
|
||||||
child_text += buildBossString(reward, color, world)
|
child_text += buildBossString(reward, color, world)
|
||||||
child_text += getHint('Child Altar Text End', world.clearer_hints).text
|
child_text += getHint('Child Altar Text End', world.hint_rng, world.clearer_hints).text
|
||||||
child_text += '\x0B'
|
child_text += '\x0B'
|
||||||
update_message_by_id(messages, 0x707A, get_raw_text(child_text), 0x20)
|
update_message_by_id(messages, 0x707A, get_raw_text(child_text), 0x20)
|
||||||
|
|
||||||
# text that appears at altar as an adult.
|
# text that appears at altar as an adult.
|
||||||
adult_text = '\x08'
|
adult_text = '\x08'
|
||||||
adult_text += getHint('Adult Altar Text Start', world.clearer_hints).text + '\x04'
|
adult_text += getHint('Adult Altar Text Start', world.hint_rng, world.clearer_hints).text + '\x04'
|
||||||
if include_rewards:
|
if include_rewards:
|
||||||
bossRewardsMedallions = [
|
bossRewardsMedallions = [
|
||||||
('Light Medallion', 'Light Blue'),
|
('Light Medallion', 'Light Blue'),
|
||||||
@@ -1029,7 +1029,7 @@ def buildAltarHints(world, messages, include_rewards=True, include_wincons=True)
|
|||||||
adult_text += '\x04'
|
adult_text += '\x04'
|
||||||
adult_text += buildGanonBossKeyString(world)
|
adult_text += buildGanonBossKeyString(world)
|
||||||
else:
|
else:
|
||||||
adult_text += getHint('Adult Altar Text End', world.clearer_hints).text
|
adult_text += getHint('Adult Altar Text End', world.hint_rng, world.clearer_hints).text
|
||||||
adult_text += '\x0B'
|
adult_text += '\x0B'
|
||||||
update_message_by_id(messages, 0x7057, get_raw_text(adult_text), 0x20)
|
update_message_by_id(messages, 0x7057, get_raw_text(adult_text), 0x20)
|
||||||
|
|
||||||
@@ -1044,7 +1044,7 @@ def buildBossString(reward, color, world):
|
|||||||
text = GossipText(f"\x08\x13{item_icon}One in #@'s pocket#...", [color], prefix='')
|
text = GossipText(f"\x08\x13{item_icon}One in #@'s pocket#...", [color], prefix='')
|
||||||
else:
|
else:
|
||||||
location = world.hinted_dungeon_reward_locations[reward]
|
location = world.hinted_dungeon_reward_locations[reward]
|
||||||
location_text = HintArea.at(location).text(world.clearer_hints, preposition=True)
|
location_text = HintArea.at(location).text(world.hint_rng, world.clearer_hints, preposition=True)
|
||||||
text = GossipText(f"\x08\x13{item_icon}One {location_text}...", [color], prefix='')
|
text = GossipText(f"\x08\x13{item_icon}One {location_text}...", [color], prefix='')
|
||||||
return str(text) + '\x04'
|
return str(text) + '\x04'
|
||||||
|
|
||||||
@@ -1054,7 +1054,7 @@ def buildBridgeReqsString(world):
|
|||||||
if world.bridge == 'open':
|
if world.bridge == 'open':
|
||||||
string += "The awakened ones will have #already created a bridge# to the castle where the evil dwells."
|
string += "The awakened ones will have #already created a bridge# to the castle where the evil dwells."
|
||||||
else:
|
else:
|
||||||
item_req_string = getHint('bridge_' + world.bridge, world.clearer_hints).text
|
item_req_string = getHint('bridge_' + world.bridge, world.hint_rng, world.clearer_hints).text
|
||||||
if world.bridge == 'medallions':
|
if world.bridge == 'medallions':
|
||||||
item_req_string = str(world.bridge_medallions) + ' ' + item_req_string
|
item_req_string = str(world.bridge_medallions) + ' ' + item_req_string
|
||||||
elif world.bridge == 'stones':
|
elif world.bridge == 'stones':
|
||||||
@@ -1077,7 +1077,7 @@ def buildGanonBossKeyString(world):
|
|||||||
string += "And the door to the \x05\x41evil one\x05\x40's chamber will be left #unlocked#."
|
string += "And the door to the \x05\x41evil one\x05\x40's chamber will be left #unlocked#."
|
||||||
else:
|
else:
|
||||||
if world.shuffle_ganon_bosskey == 'on_lacs':
|
if world.shuffle_ganon_bosskey == 'on_lacs':
|
||||||
item_req_string = getHint('lacs_' + world.lacs_condition, world.clearer_hints).text
|
item_req_string = getHint('lacs_' + world.lacs_condition, world.hint_rng, world.clearer_hints).text
|
||||||
if world.lacs_condition == 'medallions':
|
if world.lacs_condition == 'medallions':
|
||||||
item_req_string = str(world.lacs_medallions) + ' ' + item_req_string
|
item_req_string = str(world.lacs_medallions) + ' ' + item_req_string
|
||||||
elif world.lacs_condition == 'stones':
|
elif world.lacs_condition == 'stones':
|
||||||
@@ -1092,7 +1092,7 @@ def buildGanonBossKeyString(world):
|
|||||||
item_req_string = '#%s#' % item_req_string
|
item_req_string = '#%s#' % item_req_string
|
||||||
bk_location_string = "provided by Zelda once %s are retrieved" % item_req_string
|
bk_location_string = "provided by Zelda once %s are retrieved" % item_req_string
|
||||||
elif world.shuffle_ganon_bosskey in ['stones', 'medallions', 'dungeons', 'tokens', 'hearts']:
|
elif world.shuffle_ganon_bosskey in ['stones', 'medallions', 'dungeons', 'tokens', 'hearts']:
|
||||||
item_req_string = getHint('ganonBK_' + world.shuffle_ganon_bosskey, world.clearer_hints).text
|
item_req_string = getHint('ganonBK_' + world.shuffle_ganon_bosskey, world.hint_rng, world.clearer_hints).text
|
||||||
if world.shuffle_ganon_bosskey == 'medallions':
|
if world.shuffle_ganon_bosskey == 'medallions':
|
||||||
item_req_string = str(world.ganon_bosskey_medallions) + ' ' + item_req_string
|
item_req_string = str(world.ganon_bosskey_medallions) + ' ' + item_req_string
|
||||||
elif world.shuffle_ganon_bosskey == 'stones':
|
elif world.shuffle_ganon_bosskey == 'stones':
|
||||||
@@ -1107,7 +1107,7 @@ def buildGanonBossKeyString(world):
|
|||||||
item_req_string = '#%s#' % item_req_string
|
item_req_string = '#%s#' % item_req_string
|
||||||
bk_location_string = "automatically granted once %s are retrieved" % item_req_string
|
bk_location_string = "automatically granted once %s are retrieved" % item_req_string
|
||||||
else:
|
else:
|
||||||
bk_location_string = getHint('ganonBK_' + world.shuffle_ganon_bosskey, world.clearer_hints).text
|
bk_location_string = getHint('ganonBK_' + world.shuffle_ganon_bosskey, world.hint_rng, world.clearer_hints).text
|
||||||
string += "And the \x05\x41evil one\x05\x40's key will be %s." % bk_location_string
|
string += "And the \x05\x41evil one\x05\x40's key will be %s." % bk_location_string
|
||||||
return str(GossipText(string, ['Yellow'], prefix=''))
|
return str(GossipText(string, ['Yellow'], prefix=''))
|
||||||
|
|
||||||
@@ -1142,16 +1142,16 @@ def buildMiscItemHints(world, messages):
|
|||||||
if location.player != world.player:
|
if location.player != world.player:
|
||||||
player_text = world.multiworld.get_player_name(location.player) + "'s "
|
player_text = world.multiworld.get_player_name(location.player) + "'s "
|
||||||
if location.game == 'Ocarina of Time':
|
if location.game == 'Ocarina of Time':
|
||||||
area = HintArea.at(location, use_alt_hint=data['use_alt_hint']).text(world.clearer_hints, world=None)
|
area = HintArea.at(location, use_alt_hint=data['use_alt_hint']).text(world.hint_rng, world.clearer_hints, world=None)
|
||||||
else:
|
else:
|
||||||
area = location.name
|
area = location.name
|
||||||
text = data['default_item_text'].format(area=rom_safe_text(player_text + area))
|
text = data['default_item_text'].format(area=rom_safe_text(player_text + area))
|
||||||
elif 'default_item_fallback' in data:
|
elif 'default_item_fallback' in data:
|
||||||
text = data['default_item_fallback']
|
text = data['default_item_fallback']
|
||||||
else:
|
else:
|
||||||
text = getHint('Validation Line', world.clearer_hints).text
|
text = getHint('Validation Line', world.hint_rng, world.clearer_hints).text
|
||||||
location = world.get_location('Ganons Tower Boss Key Chest')
|
location = world.get_location('Ganons Tower Boss Key Chest')
|
||||||
text += f"#{getHint(getItemGenericName(location.item), world.clearer_hints).text}#"
|
text += f"#{getHint(getItemGenericName(location.item), world.hint_rng, world.clearer_hints).text}#"
|
||||||
for find, replace in data.get('replace', {}).items():
|
for find, replace in data.get('replace', {}).items():
|
||||||
text = text.replace(find, replace)
|
text = text.replace(find, replace)
|
||||||
|
|
||||||
@@ -1165,7 +1165,7 @@ def buildMiscLocationHints(world, messages):
|
|||||||
if hint_type in world.misc_hints:
|
if hint_type in world.misc_hints:
|
||||||
location = world.get_location(data['item_location'])
|
location = world.get_location(data['item_location'])
|
||||||
item = location.item
|
item = location.item
|
||||||
item_text = getHint(getItemGenericName(item), world.clearer_hints).text
|
item_text = getHint(getItemGenericName(item), world.hint_rng, world.clearer_hints).text
|
||||||
if item.player != world.player:
|
if item.player != world.player:
|
||||||
item_text += f' for {world.multiworld.get_player_name(item.player)}'
|
item_text += f' for {world.multiworld.get_player_name(item.player)}'
|
||||||
text = data['location_text'].format(item=rom_safe_text(item_text))
|
text = data['location_text'].format(item=rom_safe_text(item_text))
|
||||||
|
|||||||
@@ -295,16 +295,14 @@ random = None
|
|||||||
|
|
||||||
def get_junk_pool(ootworld):
|
def get_junk_pool(ootworld):
|
||||||
junk_pool[:] = list(junk_pool_base)
|
junk_pool[:] = list(junk_pool_base)
|
||||||
if ootworld.junk_ice_traps == 'on':
|
if ootworld.options.junk_ice_traps == 'on':
|
||||||
junk_pool.append(('Ice Trap', 10))
|
junk_pool.append(('Ice Trap', 10))
|
||||||
elif ootworld.junk_ice_traps in ['mayhem', 'onslaught']:
|
elif ootworld.options.junk_ice_traps in ['mayhem', 'onslaught']:
|
||||||
junk_pool[:] = [('Ice Trap', 1)]
|
junk_pool[:] = [('Ice Trap', 1)]
|
||||||
return junk_pool
|
return junk_pool
|
||||||
|
|
||||||
|
|
||||||
def get_junk_item(count=1, pool=None, plando_pool=None):
|
def get_junk_item(rand, count=1, pool=None, plando_pool=None):
|
||||||
global random
|
|
||||||
|
|
||||||
if count < 1:
|
if count < 1:
|
||||||
raise ValueError("get_junk_item argument 'count' must be greater than 0.")
|
raise ValueError("get_junk_item argument 'count' must be greater than 0.")
|
||||||
|
|
||||||
@@ -323,17 +321,17 @@ def get_junk_item(count=1, pool=None, plando_pool=None):
|
|||||||
raise RuntimeError("Not enough junk is available in the item pool to replace removed items.")
|
raise RuntimeError("Not enough junk is available in the item pool to replace removed items.")
|
||||||
else:
|
else:
|
||||||
junk_items, junk_weights = zip(*junk_pool)
|
junk_items, junk_weights = zip(*junk_pool)
|
||||||
return_pool.extend(random.choices(junk_items, weights=junk_weights, k=count))
|
return_pool.extend(rand.choices(junk_items, weights=junk_weights, k=count))
|
||||||
|
|
||||||
return return_pool
|
return return_pool
|
||||||
|
|
||||||
|
|
||||||
def replace_max_item(items, item, max):
|
def replace_max_item(items, item, max, rand):
|
||||||
count = 0
|
count = 0
|
||||||
for i,val in enumerate(items):
|
for i,val in enumerate(items):
|
||||||
if val == item:
|
if val == item:
|
||||||
if count >= max:
|
if count >= max:
|
||||||
items[i] = get_junk_item()[0]
|
items[i] = get_junk_item(rand)[0]
|
||||||
count += 1
|
count += 1
|
||||||
|
|
||||||
|
|
||||||
@@ -375,7 +373,7 @@ def get_pool_core(world):
|
|||||||
pending_junk_pool.append('Kokiri Sword')
|
pending_junk_pool.append('Kokiri Sword')
|
||||||
if world.shuffle_ocarinas:
|
if world.shuffle_ocarinas:
|
||||||
pending_junk_pool.append('Ocarina')
|
pending_junk_pool.append('Ocarina')
|
||||||
if world.shuffle_beans and world.multiworld.start_inventory[world.player].value.get('Magic Bean Pack', 0):
|
if world.shuffle_beans and world.options.start_inventory.value.get('Magic Bean Pack', 0):
|
||||||
pending_junk_pool.append('Magic Bean Pack')
|
pending_junk_pool.append('Magic Bean Pack')
|
||||||
if (world.gerudo_fortress != "open"
|
if (world.gerudo_fortress != "open"
|
||||||
and world.shuffle_hideoutkeys in ['any_dungeon', 'overworld', 'keysanity', 'regional']):
|
and world.shuffle_hideoutkeys in ['any_dungeon', 'overworld', 'keysanity', 'regional']):
|
||||||
@@ -450,7 +448,7 @@ def get_pool_core(world):
|
|||||||
else:
|
else:
|
||||||
item = deku_scrubs_items[location.vanilla_item]
|
item = deku_scrubs_items[location.vanilla_item]
|
||||||
if isinstance(item, list):
|
if isinstance(item, list):
|
||||||
item = random.choices([i[0] for i in item], weights=[i[1] for i in item], k=1)[0]
|
item = world.random.choices([i[0] for i in item], weights=[i[1] for i in item], k=1)[0]
|
||||||
shuffle_item = True
|
shuffle_item = True
|
||||||
|
|
||||||
# Kokiri Sword
|
# Kokiri Sword
|
||||||
@@ -489,7 +487,7 @@ def get_pool_core(world):
|
|||||||
# Cows
|
# Cows
|
||||||
elif location.vanilla_item == 'Milk':
|
elif location.vanilla_item == 'Milk':
|
||||||
if world.shuffle_cows:
|
if world.shuffle_cows:
|
||||||
item = get_junk_item()[0]
|
item = get_junk_item(world.random)[0]
|
||||||
shuffle_item = world.shuffle_cows
|
shuffle_item = world.shuffle_cows
|
||||||
if not shuffle_item:
|
if not shuffle_item:
|
||||||
location.show_in_spoiler = False
|
location.show_in_spoiler = False
|
||||||
@@ -508,13 +506,13 @@ def get_pool_core(world):
|
|||||||
item = 'Rutos Letter'
|
item = 'Rutos Letter'
|
||||||
ruto_bottles -= 1
|
ruto_bottles -= 1
|
||||||
else:
|
else:
|
||||||
item = random.choice(normal_bottles)
|
item = world.random.choice(normal_bottles)
|
||||||
shuffle_item = True
|
shuffle_item = True
|
||||||
|
|
||||||
# Magic Beans
|
# Magic Beans
|
||||||
elif location.vanilla_item == 'Buy Magic Bean':
|
elif location.vanilla_item == 'Buy Magic Bean':
|
||||||
if world.shuffle_beans:
|
if world.shuffle_beans:
|
||||||
item = 'Magic Bean Pack' if not world.multiworld.start_inventory[world.player].value.get('Magic Bean Pack', 0) else get_junk_item()[0]
|
item = 'Magic Bean Pack' if not world.options.start_inventory.value.get('Magic Bean Pack', 0) else get_junk_item(world.random)[0]
|
||||||
shuffle_item = world.shuffle_beans
|
shuffle_item = world.shuffle_beans
|
||||||
if not shuffle_item:
|
if not shuffle_item:
|
||||||
location.show_in_spoiler = False
|
location.show_in_spoiler = False
|
||||||
@@ -528,7 +526,7 @@ def get_pool_core(world):
|
|||||||
# Adult Trade Item
|
# Adult Trade Item
|
||||||
elif location.vanilla_item == 'Pocket Egg':
|
elif location.vanilla_item == 'Pocket Egg':
|
||||||
potential_trade_items = world.adult_trade_start if world.adult_trade_start else trade_items
|
potential_trade_items = world.adult_trade_start if world.adult_trade_start else trade_items
|
||||||
item = random.choice(sorted(potential_trade_items))
|
item = world.random.choice(sorted(potential_trade_items))
|
||||||
world.selected_adult_trade_item = item
|
world.selected_adult_trade_item = item
|
||||||
shuffle_item = True
|
shuffle_item = True
|
||||||
|
|
||||||
@@ -541,7 +539,7 @@ def get_pool_core(world):
|
|||||||
shuffle_item = False
|
shuffle_item = False
|
||||||
location.show_in_spoiler = False
|
location.show_in_spoiler = False
|
||||||
if shuffle_item and world.gerudo_fortress == 'normal' and 'Thieves Hideout' in world.key_rings:
|
if shuffle_item and world.gerudo_fortress == 'normal' and 'Thieves Hideout' in world.key_rings:
|
||||||
item = get_junk_item()[0] if location.name != 'Hideout 1 Torch Jail Gerudo Key' else 'Small Key Ring (Thieves Hideout)'
|
item = get_junk_item(world.random)[0] if location.name != 'Hideout 1 Torch Jail Gerudo Key' else 'Small Key Ring (Thieves Hideout)'
|
||||||
|
|
||||||
# Freestanding Rupees and Hearts
|
# Freestanding Rupees and Hearts
|
||||||
elif location.type in ['ActorOverride', 'Freestanding', 'RupeeTower']:
|
elif location.type in ['ActorOverride', 'Freestanding', 'RupeeTower']:
|
||||||
@@ -618,7 +616,7 @@ def get_pool_core(world):
|
|||||||
elif dungeon.name in world.key_rings and not dungeon.small_keys:
|
elif dungeon.name in world.key_rings and not dungeon.small_keys:
|
||||||
item = dungeon.item_name("Small Key Ring")
|
item = dungeon.item_name("Small Key Ring")
|
||||||
elif dungeon.name in world.key_rings:
|
elif dungeon.name in world.key_rings:
|
||||||
item = get_junk_item()[0]
|
item = get_junk_item(world.random)[0]
|
||||||
shuffle_item = True
|
shuffle_item = True
|
||||||
# Any other item in a dungeon.
|
# Any other item in a dungeon.
|
||||||
elif location.type in ["Chest", "NPC", "Song", "Collectable", "Cutscene", "BossHeart"]:
|
elif location.type in ["Chest", "NPC", "Song", "Collectable", "Cutscene", "BossHeart"]:
|
||||||
@@ -630,7 +628,7 @@ def get_pool_core(world):
|
|||||||
if shuffle_setting in ['remove', 'startwith']:
|
if shuffle_setting in ['remove', 'startwith']:
|
||||||
world.multiworld.push_precollected(dungeon_collection[-1])
|
world.multiworld.push_precollected(dungeon_collection[-1])
|
||||||
world.remove_from_start_inventory.append(dungeon_collection[-1].name)
|
world.remove_from_start_inventory.append(dungeon_collection[-1].name)
|
||||||
item = get_junk_item()[0]
|
item = get_junk_item(world.random)[0]
|
||||||
shuffle_item = True
|
shuffle_item = True
|
||||||
elif shuffle_setting in ['any_dungeon', 'overworld', 'regional']:
|
elif shuffle_setting in ['any_dungeon', 'overworld', 'regional']:
|
||||||
dungeon_collection[-1].priority = True
|
dungeon_collection[-1].priority = True
|
||||||
@@ -658,9 +656,9 @@ def get_pool_core(world):
|
|||||||
shop_non_item_count = len(world.shop_prices)
|
shop_non_item_count = len(world.shop_prices)
|
||||||
shop_item_count = shop_slots_count - shop_non_item_count
|
shop_item_count = shop_slots_count - shop_non_item_count
|
||||||
|
|
||||||
pool.extend(random.sample(remain_shop_items, shop_item_count))
|
pool.extend(world.random.sample(remain_shop_items, shop_item_count))
|
||||||
if shop_non_item_count:
|
if shop_non_item_count:
|
||||||
pool.extend(get_junk_item(shop_non_item_count))
|
pool.extend(get_junk_item(world.random, shop_non_item_count))
|
||||||
|
|
||||||
# Extra rupees for shopsanity.
|
# Extra rupees for shopsanity.
|
||||||
if world.shopsanity not in ['off', '0']:
|
if world.shopsanity not in ['off', '0']:
|
||||||
@@ -706,19 +704,19 @@ def get_pool_core(world):
|
|||||||
|
|
||||||
if world.shuffle_ganon_bosskey in ['stones', 'medallions', 'dungeons', 'tokens', 'hearts', 'triforce']:
|
if world.shuffle_ganon_bosskey in ['stones', 'medallions', 'dungeons', 'tokens', 'hearts', 'triforce']:
|
||||||
placed_items['Gift from Sages'] = 'Boss Key (Ganons Castle)'
|
placed_items['Gift from Sages'] = 'Boss Key (Ganons Castle)'
|
||||||
pool.extend(get_junk_item())
|
pool.extend(get_junk_item(world.random))
|
||||||
else:
|
else:
|
||||||
placed_items['Gift from Sages'] = IGNORE_LOCATION
|
placed_items['Gift from Sages'] = IGNORE_LOCATION
|
||||||
world.get_location('Gift from Sages').show_in_spoiler = False
|
world.get_location('Gift from Sages').show_in_spoiler = False
|
||||||
|
|
||||||
if world.junk_ice_traps == 'off':
|
if world.junk_ice_traps == 'off':
|
||||||
replace_max_item(pool, 'Ice Trap', 0)
|
replace_max_item(pool, 'Ice Trap', 0, world.random)
|
||||||
elif world.junk_ice_traps == 'onslaught':
|
elif world.junk_ice_traps == 'onslaught':
|
||||||
for item in [item for item, weight in junk_pool_base] + ['Recovery Heart', 'Bombs (20)', 'Arrows (30)']:
|
for item in [item for item, weight in junk_pool_base] + ['Recovery Heart', 'Bombs (20)', 'Arrows (30)']:
|
||||||
replace_max_item(pool, item, 0)
|
replace_max_item(pool, item, 0, world.random)
|
||||||
|
|
||||||
for item, maximum in item_difficulty_max[world.item_pool_value].items():
|
for item, maximum in item_difficulty_max[world.item_pool_value].items():
|
||||||
replace_max_item(pool, item, maximum)
|
replace_max_item(pool, item, maximum, world.random)
|
||||||
|
|
||||||
# world.distribution.alter_pool(world, pool)
|
# world.distribution.alter_pool(world, pool)
|
||||||
|
|
||||||
@@ -748,7 +746,7 @@ def get_pool_core(world):
|
|||||||
pending_item = pending_junk_pool.pop()
|
pending_item = pending_junk_pool.pop()
|
||||||
if not junk_candidates:
|
if not junk_candidates:
|
||||||
raise RuntimeError("Not enough junk exists in item pool for %s (+%d others) to be added." % (pending_item, len(pending_junk_pool) - 1))
|
raise RuntimeError("Not enough junk exists in item pool for %s (+%d others) to be added." % (pending_item, len(pending_junk_pool) - 1))
|
||||||
junk_item = random.choice(junk_candidates)
|
junk_item = world.random.choice(junk_candidates)
|
||||||
junk_candidates.remove(junk_item)
|
junk_candidates.remove(junk_item)
|
||||||
pool.remove(junk_item)
|
pool.remove(junk_item)
|
||||||
pool.append(pending_item)
|
pool.append(pending_item)
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
# text details: https://wiki.cloudmodding.com/oot/Text_Format
|
# text details: https://wiki.cloudmodding.com/oot/Text_Format
|
||||||
|
|
||||||
import random
|
|
||||||
from .HintList import misc_item_hint_table, misc_location_hint_table
|
from .HintList import misc_item_hint_table, misc_location_hint_table
|
||||||
from .TextBox import line_wrap
|
from .TextBox import line_wrap
|
||||||
from .Utils import find_last
|
from .Utils import find_last
|
||||||
@@ -969,7 +968,7 @@ def repack_messages(rom, messages, permutation=None, always_allow_skip=True, spe
|
|||||||
rom.write_bytes(entry_offset, [0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])
|
rom.write_bytes(entry_offset, [0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])
|
||||||
|
|
||||||
# shuffles the messages in the game, making sure to keep various message types in their own group
|
# shuffles the messages in the game, making sure to keep various message types in their own group
|
||||||
def shuffle_messages(messages, except_hints=True, always_allow_skip=True):
|
def shuffle_messages(messages, rand, except_hints=True, always_allow_skip=True):
|
||||||
|
|
||||||
permutation = [i for i, _ in enumerate(messages)]
|
permutation = [i for i, _ in enumerate(messages)]
|
||||||
|
|
||||||
@@ -1002,7 +1001,7 @@ def shuffle_messages(messages, except_hints=True, always_allow_skip=True):
|
|||||||
|
|
||||||
def shuffle_group(group):
|
def shuffle_group(group):
|
||||||
group_permutation = [i for i, _ in enumerate(group)]
|
group_permutation = [i for i, _ in enumerate(group)]
|
||||||
random.shuffle(group_permutation)
|
rand.shuffle(group_permutation)
|
||||||
|
|
||||||
for index_from, index_to in enumerate(group_permutation):
|
for index_from, index_to in enumerate(group_permutation):
|
||||||
permutation[group[index_to].index] = group[index_from].index
|
permutation[group[index_to].index] = group[index_from].index
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
#Much of this is heavily inspired from and/or based on az64's / Deathbasket's MM randomizer
|
#Much of this is heavily inspired from and/or based on az64's / Deathbasket's MM randomizer
|
||||||
|
|
||||||
import random
|
|
||||||
import os
|
import os
|
||||||
from .Utils import compare_version, data_path
|
from .Utils import compare_version, data_path
|
||||||
|
|
||||||
@@ -175,7 +174,7 @@ def process_sequences(rom, sequences, target_sequences, disabled_source_sequence
|
|||||||
return sequences, target_sequences
|
return sequences, target_sequences
|
||||||
|
|
||||||
|
|
||||||
def shuffle_music(sequences, target_sequences, music_mapping, log):
|
def shuffle_music(sequences, target_sequences, music_mapping, log, rand):
|
||||||
sequence_dict = {}
|
sequence_dict = {}
|
||||||
sequence_ids = []
|
sequence_ids = []
|
||||||
|
|
||||||
@@ -191,7 +190,7 @@ def shuffle_music(sequences, target_sequences, music_mapping, log):
|
|||||||
# Shuffle the sequences
|
# Shuffle the sequences
|
||||||
if len(sequences) < len(target_sequences):
|
if len(sequences) < len(target_sequences):
|
||||||
raise Exception(f"Not enough custom music/fanfares ({len(sequences)}) to omit base Ocarina of Time sequences ({len(target_sequences)}).")
|
raise Exception(f"Not enough custom music/fanfares ({len(sequences)}) to omit base Ocarina of Time sequences ({len(target_sequences)}).")
|
||||||
random.shuffle(sequence_ids)
|
rand.shuffle(sequence_ids)
|
||||||
|
|
||||||
sequences = []
|
sequences = []
|
||||||
for target_sequence in target_sequences:
|
for target_sequence in target_sequences:
|
||||||
@@ -328,7 +327,7 @@ def rebuild_sequences(rom, sequences):
|
|||||||
rom.write_byte(base, j.instrument_set)
|
rom.write_byte(base, j.instrument_set)
|
||||||
|
|
||||||
|
|
||||||
def shuffle_pointers_table(rom, ids, music_mapping, log):
|
def shuffle_pointers_table(rom, ids, music_mapping, log, rand):
|
||||||
# Read in all the Music data
|
# Read in all the Music data
|
||||||
bgm_data = {}
|
bgm_data = {}
|
||||||
bgm_ids = []
|
bgm_ids = []
|
||||||
@@ -341,7 +340,7 @@ def shuffle_pointers_table(rom, ids, music_mapping, log):
|
|||||||
bgm_ids.append(bgm[0])
|
bgm_ids.append(bgm[0])
|
||||||
|
|
||||||
# shuffle data
|
# shuffle data
|
||||||
random.shuffle(bgm_ids)
|
rand.shuffle(bgm_ids)
|
||||||
|
|
||||||
# Write Music data back in random ordering
|
# Write Music data back in random ordering
|
||||||
for bgm in ids:
|
for bgm in ids:
|
||||||
@@ -424,13 +423,13 @@ def randomize_music(rom, ootworld, music_mapping):
|
|||||||
# process_sequences(rom, sequences, target_sequences, disabled_source_sequences, disabled_target_sequences, bgm_ids)
|
# process_sequences(rom, sequences, target_sequences, disabled_source_sequences, disabled_target_sequences, bgm_ids)
|
||||||
# if ootworld.background_music == 'random_custom_only':
|
# if ootworld.background_music == 'random_custom_only':
|
||||||
# sequences = [seq for seq in sequences if seq.cosmetic_name not in [x[0] for x in bgm_ids] or seq.cosmetic_name in music_mapping.values()]
|
# sequences = [seq for seq in sequences if seq.cosmetic_name not in [x[0] for x in bgm_ids] or seq.cosmetic_name in music_mapping.values()]
|
||||||
# sequences, log = shuffle_music(sequences, target_sequences, music_mapping, log)
|
# sequences, log = shuffle_music(sequences, target_sequences, music_mapping, log, ootworld.random)
|
||||||
|
|
||||||
# if ootworld.fanfares in ['random', 'random_custom_only'] or ff_mapped or ocarina_mapped:
|
# if ootworld.fanfares in ['random', 'random_custom_only'] or ff_mapped or ocarina_mapped:
|
||||||
# process_sequences(rom, fanfare_sequences, fanfare_target_sequences, disabled_source_sequences, disabled_target_sequences, ff_ids, 'fanfare')
|
# process_sequences(rom, fanfare_sequences, fanfare_target_sequences, disabled_source_sequences, disabled_target_sequences, ff_ids, 'fanfare')
|
||||||
# if ootworld.fanfares == 'random_custom_only':
|
# if ootworld.fanfares == 'random_custom_only':
|
||||||
# fanfare_sequences = [seq for seq in fanfare_sequences if seq.cosmetic_name not in [x[0] for x in fanfare_sequence_ids] or seq.cosmetic_name in music_mapping.values()]
|
# fanfare_sequences = [seq for seq in fanfare_sequences if seq.cosmetic_name not in [x[0] for x in fanfare_sequence_ids] or seq.cosmetic_name in music_mapping.values()]
|
||||||
# fanfare_sequences, log = shuffle_music(fanfare_sequences, fanfare_target_sequences, music_mapping, log)
|
# fanfare_sequences, log = shuffle_music(fanfare_sequences, fanfare_target_sequences, music_mapping, log, ootworld.random)
|
||||||
|
|
||||||
# if disabled_source_sequences:
|
# if disabled_source_sequences:
|
||||||
# log = disable_music(rom, disabled_source_sequences.values(), log)
|
# log = disable_music(rom, disabled_source_sequences.values(), log)
|
||||||
@@ -438,10 +437,10 @@ def randomize_music(rom, ootworld, music_mapping):
|
|||||||
# rebuild_sequences(rom, sequences + fanfare_sequences)
|
# rebuild_sequences(rom, sequences + fanfare_sequences)
|
||||||
# else:
|
# else:
|
||||||
if ootworld.background_music == 'randomized' or bgm_mapped:
|
if ootworld.background_music == 'randomized' or bgm_mapped:
|
||||||
log = shuffle_pointers_table(rom, bgm_ids, music_mapping, log)
|
log = shuffle_pointers_table(rom, bgm_ids, music_mapping, log, ootworld.random)
|
||||||
|
|
||||||
if ootworld.fanfares == 'randomized' or ff_mapped or ocarina_mapped:
|
if ootworld.fanfares == 'randomized' or ff_mapped or ocarina_mapped:
|
||||||
log = shuffle_pointers_table(rom, ff_ids, music_mapping, log)
|
log = shuffle_pointers_table(rom, ff_ids, music_mapping, log, ootworld.random)
|
||||||
# end_else
|
# end_else
|
||||||
if disabled_target_sequences:
|
if disabled_target_sequences:
|
||||||
log = disable_music(rom, disabled_target_sequences.values(), log)
|
log = disable_music(rom, disabled_target_sequences.values(), log)
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import struct
|
import struct
|
||||||
import random
|
|
||||||
import io
|
import io
|
||||||
import array
|
import array
|
||||||
import zlib
|
import zlib
|
||||||
@@ -88,7 +87,7 @@ def write_block_section(start, key_skip, in_data, patch_data, is_continue):
|
|||||||
# xor_range is the range the XOR key will read from. This range is not
|
# xor_range is the range the XOR key will read from. This range is not
|
||||||
# too important, but I tried to choose from a section that didn't really
|
# too important, but I tried to choose from a section that didn't really
|
||||||
# have big gaps of 0s which we want to avoid.
|
# have big gaps of 0s which we want to avoid.
|
||||||
def create_patch_file(rom, xor_range=(0x00B8AD30, 0x00F029A0)):
|
def create_patch_file(rom, rand, xor_range=(0x00B8AD30, 0x00F029A0)):
|
||||||
dma_start, dma_end = rom.get_dma_table_range()
|
dma_start, dma_end = rom.get_dma_table_range()
|
||||||
|
|
||||||
# add header
|
# add header
|
||||||
@@ -100,7 +99,7 @@ def create_patch_file(rom, xor_range=(0x00B8AD30, 0x00F029A0)):
|
|||||||
|
|
||||||
# get random xor key. This range is chosen because it generally
|
# get random xor key. This range is chosen because it generally
|
||||||
# doesn't have many sections of 0s
|
# doesn't have many sections of 0s
|
||||||
xor_address = random.Random().randint(*xor_range)
|
xor_address = rand.randint(*xor_range)
|
||||||
patch_data.append_int32(xor_address)
|
patch_data.append_int32(xor_address)
|
||||||
|
|
||||||
new_buffer = copy.copy(rom.original.buffer)
|
new_buffer = copy.copy(rom.original.buffer)
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import typing
|
import typing
|
||||||
import random
|
import random
|
||||||
from Options import Option, DefaultOnToggle, Toggle, Range, OptionList, OptionSet, DeathLink, PlandoConnections
|
from dataclasses import dataclass
|
||||||
|
from Options import Option, DefaultOnToggle, Toggle, Range, OptionList, OptionSet, DeathLink, PlandoConnections, \
|
||||||
|
PerGameCommonOptions, OptionGroup
|
||||||
from .EntranceShuffle import entrance_shuffle_table
|
from .EntranceShuffle import entrance_shuffle_table
|
||||||
from .LogicTricks import normalized_name_tricks
|
from .LogicTricks import normalized_name_tricks
|
||||||
from .ColorSFXOptions import *
|
from .ColorSFXOptions import *
|
||||||
@@ -1281,21 +1283,166 @@ class LogicTricks(OptionList):
|
|||||||
valid_keys_casefold = True
|
valid_keys_casefold = True
|
||||||
|
|
||||||
|
|
||||||
# All options assembled into a single dict
|
@dataclass
|
||||||
oot_options: typing.Dict[str, type(Option)] = {
|
class OoTOptions(PerGameCommonOptions):
|
||||||
"plando_connections": OoTPlandoConnections,
|
plando_connections: OoTPlandoConnections
|
||||||
"logic_rules": Logic,
|
death_link: DeathLink
|
||||||
"logic_no_night_tokens_without_suns_song": NightTokens,
|
logic_rules: Logic
|
||||||
**open_options,
|
logic_no_night_tokens_without_suns_song: NightTokens
|
||||||
**world_options,
|
logic_tricks: LogicTricks
|
||||||
**bridge_options,
|
open_forest: Forest
|
||||||
**dungeon_items_options,
|
open_kakariko: Gate
|
||||||
**shuffle_options,
|
open_door_of_time: DoorOfTime
|
||||||
**timesavers_options,
|
zora_fountain: Fountain
|
||||||
**misc_options,
|
gerudo_fortress: Fortress
|
||||||
**itempool_options,
|
bridge: Bridge
|
||||||
**cosmetic_options,
|
trials: Trials
|
||||||
**sfx_options,
|
starting_age: StartingAge
|
||||||
"logic_tricks": LogicTricks,
|
shuffle_interior_entrances: InteriorEntrances
|
||||||
"death_link": DeathLink,
|
shuffle_grotto_entrances: GrottoEntrances
|
||||||
}
|
shuffle_dungeon_entrances: DungeonEntrances
|
||||||
|
shuffle_overworld_entrances: OverworldEntrances
|
||||||
|
owl_drops: OwlDrops
|
||||||
|
warp_songs: WarpSongs
|
||||||
|
spawn_positions: SpawnPositions
|
||||||
|
shuffle_bosses: BossEntrances
|
||||||
|
# mix_entrance_pools: MixEntrancePools
|
||||||
|
# decouple_entrances: DecoupleEntrances
|
||||||
|
triforce_hunt: TriforceHunt
|
||||||
|
triforce_goal: TriforceGoal
|
||||||
|
extra_triforce_percentage: ExtraTriforces
|
||||||
|
bombchus_in_logic: LogicalChus
|
||||||
|
dungeon_shortcuts: DungeonShortcuts
|
||||||
|
dungeon_shortcuts_list: DungeonShortcutsList
|
||||||
|
mq_dungeons_mode: MQDungeons
|
||||||
|
mq_dungeons_list: MQDungeonList
|
||||||
|
mq_dungeons_count: MQDungeonCount
|
||||||
|
# empty_dungeons_mode: EmptyDungeons
|
||||||
|
# empty_dungeons_list: EmptyDungeonList
|
||||||
|
# empty_dungeon_count: EmptyDungeonCount
|
||||||
|
bridge_stones: BridgeStones
|
||||||
|
bridge_medallions: BridgeMedallions
|
||||||
|
bridge_rewards: BridgeRewards
|
||||||
|
bridge_tokens: BridgeTokens
|
||||||
|
bridge_hearts: BridgeHearts
|
||||||
|
shuffle_mapcompass: ShuffleMapCompass
|
||||||
|
shuffle_smallkeys: ShuffleKeys
|
||||||
|
shuffle_hideoutkeys: ShuffleGerudoKeys
|
||||||
|
shuffle_bosskeys: ShuffleBossKeys
|
||||||
|
enhance_map_compass: EnhanceMC
|
||||||
|
shuffle_ganon_bosskey: ShuffleGanonBK
|
||||||
|
ganon_bosskey_medallions: GanonBKMedallions
|
||||||
|
ganon_bosskey_stones: GanonBKStones
|
||||||
|
ganon_bosskey_rewards: GanonBKRewards
|
||||||
|
ganon_bosskey_tokens: GanonBKTokens
|
||||||
|
ganon_bosskey_hearts: GanonBKHearts
|
||||||
|
key_rings: KeyRings
|
||||||
|
key_rings_list: KeyRingList
|
||||||
|
shuffle_song_items: SongShuffle
|
||||||
|
shopsanity: ShopShuffle
|
||||||
|
shop_slots: ShopSlots
|
||||||
|
shopsanity_prices: ShopPrices
|
||||||
|
tokensanity: TokenShuffle
|
||||||
|
shuffle_scrubs: ScrubShuffle
|
||||||
|
shuffle_child_trade: ShuffleChildTrade
|
||||||
|
shuffle_freestanding_items: ShuffleFreestanding
|
||||||
|
shuffle_pots: ShufflePots
|
||||||
|
shuffle_crates: ShuffleCrates
|
||||||
|
shuffle_cows: ShuffleCows
|
||||||
|
shuffle_beehives: ShuffleBeehives
|
||||||
|
shuffle_kokiri_sword: ShuffleSword
|
||||||
|
shuffle_ocarinas: ShuffleOcarinas
|
||||||
|
shuffle_gerudo_card: ShuffleCard
|
||||||
|
shuffle_beans: ShuffleBeans
|
||||||
|
shuffle_medigoron_carpet_salesman: ShuffleMedigoronCarpet
|
||||||
|
shuffle_frog_song_rupees: ShuffleFrogRupees
|
||||||
|
no_escape_sequence: SkipEscape
|
||||||
|
no_guard_stealth: SkipStealth
|
||||||
|
no_epona_race: SkipEponaRace
|
||||||
|
skip_some_minigame_phases: SkipMinigamePhases
|
||||||
|
complete_mask_quest: CompleteMaskQuest
|
||||||
|
useful_cutscenes: UsefulCutscenes
|
||||||
|
fast_chests: FastChests
|
||||||
|
free_scarecrow: FreeScarecrow
|
||||||
|
fast_bunny_hood: FastBunny
|
||||||
|
plant_beans: PlantBeans
|
||||||
|
chicken_count: ChickenCount
|
||||||
|
big_poe_count: BigPoeCount
|
||||||
|
fae_torch_count: FAETorchCount
|
||||||
|
correct_chest_appearances: CorrectChestAppearance
|
||||||
|
minor_items_as_major_chest: MinorInMajor
|
||||||
|
invisible_chests: InvisibleChests
|
||||||
|
correct_potcrate_appearances: CorrectPotCrateAppearance
|
||||||
|
hints: Hints
|
||||||
|
misc_hints: MiscHints
|
||||||
|
hint_dist: HintDistribution
|
||||||
|
text_shuffle: TextShuffle
|
||||||
|
damage_multiplier: DamageMultiplier
|
||||||
|
deadly_bonks: DeadlyBonks
|
||||||
|
no_collectible_hearts: HeroMode
|
||||||
|
starting_tod: StartingToD
|
||||||
|
blue_fire_arrows: BlueFireArrows
|
||||||
|
fix_broken_drops: FixBrokenDrops
|
||||||
|
start_with_consumables: ConsumableStart
|
||||||
|
start_with_rupees: RupeeStart
|
||||||
|
item_pool_value: ItemPoolValue
|
||||||
|
junk_ice_traps: IceTraps
|
||||||
|
ice_trap_appearance: IceTrapVisual
|
||||||
|
adult_trade_start: AdultTradeStart
|
||||||
|
default_targeting: Targeting
|
||||||
|
display_dpad: DisplayDpad
|
||||||
|
dpad_dungeon_menu: DpadDungeonMenu
|
||||||
|
correct_model_colors: CorrectColors
|
||||||
|
background_music: BackgroundMusic
|
||||||
|
fanfares: Fanfares
|
||||||
|
ocarina_fanfares: OcarinaFanfares
|
||||||
|
kokiri_color: kokiri_color
|
||||||
|
goron_color: goron_color
|
||||||
|
zora_color: zora_color
|
||||||
|
silver_gauntlets_color: silver_gauntlets_color
|
||||||
|
golden_gauntlets_color: golden_gauntlets_color
|
||||||
|
mirror_shield_frame_color: mirror_shield_frame_color
|
||||||
|
navi_color_default_inner: navi_color_default_inner
|
||||||
|
navi_color_default_outer: navi_color_default_outer
|
||||||
|
navi_color_enemy_inner: navi_color_enemy_inner
|
||||||
|
navi_color_enemy_outer: navi_color_enemy_outer
|
||||||
|
navi_color_npc_inner: navi_color_npc_inner
|
||||||
|
navi_color_npc_outer: navi_color_npc_outer
|
||||||
|
navi_color_prop_inner: navi_color_prop_inner
|
||||||
|
navi_color_prop_outer: navi_color_prop_outer
|
||||||
|
sword_trail_duration: SwordTrailDuration
|
||||||
|
sword_trail_color_inner: sword_trail_color_inner
|
||||||
|
sword_trail_color_outer: sword_trail_color_outer
|
||||||
|
bombchu_trail_color_inner: bombchu_trail_color_inner
|
||||||
|
bombchu_trail_color_outer: bombchu_trail_color_outer
|
||||||
|
boomerang_trail_color_inner: boomerang_trail_color_inner
|
||||||
|
boomerang_trail_color_outer: boomerang_trail_color_outer
|
||||||
|
heart_color: heart_color
|
||||||
|
magic_color: magic_color
|
||||||
|
a_button_color: a_button_color
|
||||||
|
b_button_color: b_button_color
|
||||||
|
c_button_color: c_button_color
|
||||||
|
start_button_color: start_button_color
|
||||||
|
sfx_navi_overworld: sfx_navi_overworld
|
||||||
|
sfx_navi_enemy: sfx_navi_enemy
|
||||||
|
sfx_low_hp: sfx_low_hp
|
||||||
|
sfx_menu_cursor: sfx_menu_cursor
|
||||||
|
sfx_menu_select: sfx_menu_select
|
||||||
|
sfx_nightfall: sfx_nightfall
|
||||||
|
sfx_horse_neigh: sfx_horse_neigh
|
||||||
|
sfx_hover_boots: sfx_hover_boots
|
||||||
|
sfx_ocarina: SfxOcarina
|
||||||
|
|
||||||
|
|
||||||
|
oot_option_groups: typing.List[OptionGroup] = [
|
||||||
|
OptionGroup("Open", [option for option in open_options.values()]),
|
||||||
|
OptionGroup("World", [*[option for option in world_options.values()],
|
||||||
|
*[option for option in bridge_options.values()]]),
|
||||||
|
OptionGroup("Shuffle", [option for option in shuffle_options.values()]),
|
||||||
|
OptionGroup("Dungeon Items", [option for option in dungeon_items_options.values()]),
|
||||||
|
OptionGroup("Timesavers", [option for option in timesavers_options.values()]),
|
||||||
|
OptionGroup("Misc", [option for option in misc_options.values()]),
|
||||||
|
OptionGroup("Item Pool", [option for option in itempool_options.values()]),
|
||||||
|
OptionGroup("Cosmetics", [option for option in cosmetic_options.values()]),
|
||||||
|
OptionGroup("SFX", [option for option in sfx_options.values()])
|
||||||
|
]
|
||||||
|
|||||||
@@ -208,8 +208,8 @@ def patch_rom(world, rom):
|
|||||||
|
|
||||||
# Fix Ice Cavern Alcove Camera
|
# Fix Ice Cavern Alcove Camera
|
||||||
if not world.dungeon_mq['Ice Cavern']:
|
if not world.dungeon_mq['Ice Cavern']:
|
||||||
rom.write_byte(0x2BECA25,0x01);
|
rom.write_byte(0x2BECA25,0x01)
|
||||||
rom.write_byte(0x2BECA2D,0x01);
|
rom.write_byte(0x2BECA2D,0x01)
|
||||||
|
|
||||||
# Fix GS rewards to be static
|
# Fix GS rewards to be static
|
||||||
rom.write_int32(0xEA3934, 0)
|
rom.write_int32(0xEA3934, 0)
|
||||||
@@ -944,7 +944,7 @@ def patch_rom(world, rom):
|
|||||||
|
|
||||||
scene_table = 0x00B71440
|
scene_table = 0x00B71440
|
||||||
for scene in range(0x00, 0x65):
|
for scene in range(0x00, 0x65):
|
||||||
scene_start = rom.read_int32(scene_table + (scene * 0x14));
|
scene_start = rom.read_int32(scene_table + (scene * 0x14))
|
||||||
add_scene_exits(scene_start)
|
add_scene_exits(scene_start)
|
||||||
|
|
||||||
return exit_table
|
return exit_table
|
||||||
@@ -1632,10 +1632,10 @@ def patch_rom(world, rom):
|
|||||||
reward_text = None
|
reward_text = None
|
||||||
elif getattr(location.item, 'looks_like_item', None) is not None:
|
elif getattr(location.item, 'looks_like_item', None) is not None:
|
||||||
jabu_item = location.item.looks_like_item
|
jabu_item = location.item.looks_like_item
|
||||||
reward_text = create_fake_name(getHint(getItemGenericName(location.item.looks_like_item), True).text)
|
reward_text = create_fake_name(getHint(getItemGenericName(location.item.looks_like_item), world.hint_rng, True).text)
|
||||||
else:
|
else:
|
||||||
jabu_item = location.item
|
jabu_item = location.item
|
||||||
reward_text = getHint(getItemGenericName(location.item), True).text
|
reward_text = getHint(getItemGenericName(location.item), world.hint_rng, True).text
|
||||||
|
|
||||||
# Update "Princess Ruto got the Spiritual Stone!" text before the midboss in Jabu
|
# Update "Princess Ruto got the Spiritual Stone!" text before the midboss in Jabu
|
||||||
if reward_text is None:
|
if reward_text is None:
|
||||||
@@ -1687,7 +1687,7 @@ def patch_rom(world, rom):
|
|||||||
|
|
||||||
# Sets hooks for gossip stone changes
|
# Sets hooks for gossip stone changes
|
||||||
|
|
||||||
symbol = rom.sym("GOSSIP_HINT_CONDITION");
|
symbol = rom.sym("GOSSIP_HINT_CONDITION")
|
||||||
|
|
||||||
if world.hints == 'none':
|
if world.hints == 'none':
|
||||||
rom.write_int32(symbol, 0)
|
rom.write_int32(symbol, 0)
|
||||||
@@ -2264,9 +2264,9 @@ def patch_rom(world, rom):
|
|||||||
|
|
||||||
# text shuffle
|
# text shuffle
|
||||||
if world.text_shuffle == 'except_hints':
|
if world.text_shuffle == 'except_hints':
|
||||||
permutation = shuffle_messages(messages, except_hints=True)
|
permutation = shuffle_messages(messages, world.random, except_hints=True)
|
||||||
elif world.text_shuffle == 'complete':
|
elif world.text_shuffle == 'complete':
|
||||||
permutation = shuffle_messages(messages, except_hints=False)
|
permutation = shuffle_messages(messages, world.random, except_hints=False)
|
||||||
|
|
||||||
# update warp song preview text boxes
|
# update warp song preview text boxes
|
||||||
update_warp_song_text(messages, world)
|
update_warp_song_text(messages, world)
|
||||||
@@ -2358,7 +2358,7 @@ def patch_rom(world, rom):
|
|||||||
|
|
||||||
# Write numeric seed truncated to 32 bits for rng seeding
|
# Write numeric seed truncated to 32 bits for rng seeding
|
||||||
# Overwritten with new seed every time a new rng value is generated
|
# Overwritten with new seed every time a new rng value is generated
|
||||||
rng_seed = world.multiworld.per_slot_randoms[world.player].getrandbits(32)
|
rng_seed = world.random.getrandbits(32)
|
||||||
rom.write_int32(rom.sym('RNG_SEED_INT'), rng_seed)
|
rom.write_int32(rom.sym('RNG_SEED_INT'), rng_seed)
|
||||||
# Static initial seed value for one-time random actions like the Hylian Shield discount
|
# Static initial seed value for one-time random actions like the Hylian Shield discount
|
||||||
rom.write_int32(rom.sym('RANDOMIZER_RNG_SEED'), rng_seed)
|
rom.write_int32(rom.sym('RANDOMIZER_RNG_SEED'), rng_seed)
|
||||||
@@ -2560,7 +2560,7 @@ def scene_get_actors(rom, actor_func, scene_data, scene, alternate=None, process
|
|||||||
room_count = rom.read_byte(scene_data + 1)
|
room_count = rom.read_byte(scene_data + 1)
|
||||||
room_list = scene_start + (rom.read_int32(scene_data + 4) & 0x00FFFFFF)
|
room_list = scene_start + (rom.read_int32(scene_data + 4) & 0x00FFFFFF)
|
||||||
for _ in range(0, room_count):
|
for _ in range(0, room_count):
|
||||||
room_data = rom.read_int32(room_list);
|
room_data = rom.read_int32(room_list)
|
||||||
|
|
||||||
if not room_data in processed_rooms:
|
if not room_data in processed_rooms:
|
||||||
actors.update(room_get_actors(rom, actor_func, room_data, scene))
|
actors.update(room_get_actors(rom, actor_func, room_data, scene))
|
||||||
@@ -2591,7 +2591,7 @@ def get_actor_list(rom, actor_func):
|
|||||||
actors = {}
|
actors = {}
|
||||||
scene_table = 0x00B71440
|
scene_table = 0x00B71440
|
||||||
for scene in range(0x00, 0x65):
|
for scene in range(0x00, 0x65):
|
||||||
scene_data = rom.read_int32(scene_table + (scene * 0x14));
|
scene_data = rom.read_int32(scene_table + (scene * 0x14))
|
||||||
actors.update(scene_get_actors(rom, actor_func, scene_data, scene))
|
actors.update(scene_get_actors(rom, actor_func, scene_data, scene))
|
||||||
return actors
|
return actors
|
||||||
|
|
||||||
@@ -2605,7 +2605,7 @@ def get_override_itemid(override_table, scene, type, flags):
|
|||||||
def remove_entrance_blockers(rom):
|
def remove_entrance_blockers(rom):
|
||||||
def remove_entrance_blockers_do(rom, actor_id, actor, scene):
|
def remove_entrance_blockers_do(rom, actor_id, actor, scene):
|
||||||
if actor_id == 0x014E and scene == 97:
|
if actor_id == 0x014E and scene == 97:
|
||||||
actor_var = rom.read_int16(actor + 14);
|
actor_var = rom.read_int16(actor + 14)
|
||||||
if actor_var == 0xFF01:
|
if actor_var == 0xFF01:
|
||||||
rom.write_int16(actor + 14, 0x0700)
|
rom.write_int16(actor + 14, 0x0700)
|
||||||
get_actor_list(rom, remove_entrance_blockers_do)
|
get_actor_list(rom, remove_entrance_blockers_do)
|
||||||
@@ -2789,7 +2789,7 @@ def place_shop_items(rom, world, shop_items, messages, locations, init_shop_id=F
|
|||||||
purchase_text = '\x08%s %d Rupees\x09\x01%s\x01\x1B\x05\x42Buy\x01Don\'t buy\x05\x40\x02' % (split_item_name[0], location.price, split_item_name[1])
|
purchase_text = '\x08%s %d Rupees\x09\x01%s\x01\x1B\x05\x42Buy\x01Don\'t buy\x05\x40\x02' % (split_item_name[0], location.price, split_item_name[1])
|
||||||
else:
|
else:
|
||||||
if item_display.game == "Ocarina of Time":
|
if item_display.game == "Ocarina of Time":
|
||||||
shop_item_name = getSimpleHintNoPrefix(item_display)
|
shop_item_name = getSimpleHintNoPrefix(item_display, world.random)
|
||||||
else:
|
else:
|
||||||
shop_item_name = item_display.name
|
shop_item_name = item_display.name
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user