mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-03-24 05:23:23 -07:00
Merge branch 'main' of https://github.com/Marechal-L/Archipelago
This commit is contained in:
@@ -48,6 +48,7 @@ class MultiWorld():
|
||||
state: CollectionState
|
||||
|
||||
accessibility: Dict[int, Options.Accessibility]
|
||||
early_items: Dict[int, Options.EarlyItems]
|
||||
local_items: Dict[int, Options.LocalItems]
|
||||
non_local_items: Dict[int, Options.NonLocalItems]
|
||||
progression_balancing: Dict[int, Options.ProgressionBalancing]
|
||||
|
||||
@@ -91,12 +91,18 @@ class ClientCommandProcessor(CommandProcessor):
|
||||
|
||||
def _cmd_items(self):
|
||||
"""List all item names for the currently running game."""
|
||||
if not self.ctx.game:
|
||||
self.output("No game set, cannot determine existing items.")
|
||||
return False
|
||||
self.output(f"Item Names for {self.ctx.game}")
|
||||
for item_name in AutoWorldRegister.world_types[self.ctx.game].item_name_to_id:
|
||||
self.output(item_name)
|
||||
|
||||
def _cmd_locations(self):
|
||||
"""List all location names for the currently running game."""
|
||||
if not self.ctx.game:
|
||||
self.output("No game set, cannot determine existing locations.")
|
||||
return False
|
||||
self.output(f"Location Names for {self.ctx.game}")
|
||||
for location_name in AutoWorldRegister.world_types[self.ctx.game].location_name_to_id:
|
||||
self.output(location_name)
|
||||
@@ -279,6 +285,7 @@ class CommonContext:
|
||||
self.auth = await self.console_input()
|
||||
|
||||
async def send_connect(self, **kwargs: typing.Any) -> None:
|
||||
""" send `Connect` packet to log in to server """
|
||||
payload = {
|
||||
'cmd': 'Connect',
|
||||
'password': self.password, 'name': self.auth, 'version': Utils.version_tuple,
|
||||
@@ -294,6 +301,7 @@ class CommonContext:
|
||||
return await self.input_queue.get()
|
||||
|
||||
async def connect(self, address: typing.Optional[str] = None) -> None:
|
||||
""" disconnect any previous connection, and open new connection to the server """
|
||||
await self.disconnect()
|
||||
self.server_task = asyncio.create_task(server_loop(self, address), name="server loop")
|
||||
|
||||
@@ -304,6 +312,12 @@ class CommonContext:
|
||||
return self.slot in self.slot_info[slot].group_members
|
||||
return False
|
||||
|
||||
def is_uninteresting_item_send(self, print_json_packet: dict) -> bool:
|
||||
"""Helper function for filtering out ItemSend prints that do not concern the local player."""
|
||||
return print_json_packet.get("type", "") == "ItemSend" \
|
||||
and not self.slot_concerns_self(print_json_packet["receiving"]) \
|
||||
and not self.slot_concerns_self(print_json_packet["item"].player)
|
||||
|
||||
def on_print(self, args: dict):
|
||||
logger.info(args["text"])
|
||||
|
||||
|
||||
@@ -4,9 +4,11 @@ import logging
|
||||
import json
|
||||
import string
|
||||
import copy
|
||||
import re
|
||||
import subprocess
|
||||
import time
|
||||
import random
|
||||
import typing
|
||||
|
||||
import ModuleUpdate
|
||||
ModuleUpdate.update()
|
||||
@@ -46,6 +48,13 @@ class FactorioCommandProcessor(ClientCommandProcessor):
|
||||
"""Manually trigger a resync."""
|
||||
self.ctx.awaiting_bridge = True
|
||||
|
||||
def _cmd_toggle_send_filter(self):
|
||||
"""Toggle filtering of item sends that get displayed in-game to only those that involve you."""
|
||||
self.ctx.toggle_filter_item_sends()
|
||||
|
||||
def _cmd_toggle_chat(self):
|
||||
"""Toggle sending of chat messages from players on the Factorio server to Archipelago."""
|
||||
self.ctx.toggle_bridge_chat_out()
|
||||
|
||||
class FactorioContext(CommonContext):
|
||||
command_processor = FactorioCommandProcessor
|
||||
@@ -65,6 +74,9 @@ class FactorioContext(CommonContext):
|
||||
self.factorio_json_text_parser = FactorioJSONtoTextParser(self)
|
||||
self.energy_link_increment = 0
|
||||
self.last_deplete = 0
|
||||
self.filter_item_sends: bool = False
|
||||
self.multiplayer: bool = False # whether multiple different players have connected
|
||||
self.bridge_chat_out: bool = True
|
||||
|
||||
async def server_auth(self, password_requested: bool = False):
|
||||
if password_requested and not self.password:
|
||||
@@ -81,12 +93,15 @@ class FactorioContext(CommonContext):
|
||||
def on_print(self, args: dict):
|
||||
super(FactorioContext, self).on_print(args)
|
||||
if self.rcon_client:
|
||||
self.print_to_game(args['text'])
|
||||
if not args['text'].startswith(self.player_names[self.slot] + ":"):
|
||||
self.print_to_game(args['text'])
|
||||
|
||||
def on_print_json(self, args: dict):
|
||||
if self.rcon_client:
|
||||
text = self.factorio_json_text_parser(copy.deepcopy(args["data"]))
|
||||
self.print_to_game(text)
|
||||
if not self.filter_item_sends or not self.is_uninteresting_item_send(args):
|
||||
text = self.factorio_json_text_parser(copy.deepcopy(args["data"]))
|
||||
if not text.startswith(self.player_names[self.slot] + ":"):
|
||||
self.print_to_game(text)
|
||||
super(FactorioContext, self).on_print_json(args)
|
||||
|
||||
@property
|
||||
@@ -123,6 +138,45 @@ class FactorioContext(CommonContext):
|
||||
f"{Utils.format_SI_prefix(args['value'])}J remaining.")
|
||||
self.rcon_client.send_command(f"/ap-energylink {gained}")
|
||||
|
||||
def on_user_say(self, text: str) -> typing.Optional[str]:
|
||||
# Mirror chat sent from the UI to the Factorio server.
|
||||
self.print_to_game(f"{self.player_names[self.slot]}: {text}")
|
||||
return text
|
||||
|
||||
async def chat_from_factorio(self, user: str, message: str) -> None:
|
||||
if not self.bridge_chat_out:
|
||||
return
|
||||
|
||||
# Pass through commands
|
||||
if message.startswith("!"):
|
||||
await self.send_msgs([{"cmd": "Say", "text": message}])
|
||||
return
|
||||
|
||||
# Omit messages that contain local coordinates
|
||||
if "[gps=" in message:
|
||||
return
|
||||
|
||||
prefix = f"({user}) " if self.multiplayer else ""
|
||||
await self.send_msgs([{"cmd": "Say", "text": f"{prefix}{message}"}])
|
||||
|
||||
def toggle_filter_item_sends(self) -> None:
|
||||
self.filter_item_sends = not self.filter_item_sends
|
||||
if self.filter_item_sends:
|
||||
announcement = "Item sends are now filtered."
|
||||
else:
|
||||
announcement = "Item sends are no longer filtered."
|
||||
logger.info(announcement)
|
||||
self.print_to_game(announcement)
|
||||
|
||||
def toggle_bridge_chat_out(self) -> None:
|
||||
self.bridge_chat_out = not self.bridge_chat_out
|
||||
if self.bridge_chat_out:
|
||||
announcement = "Chat is now bridged to Archipelago."
|
||||
else:
|
||||
announcement = "Chat is no longer bridged to Archipelago."
|
||||
logger.info(announcement)
|
||||
self.print_to_game(announcement)
|
||||
|
||||
def run_gui(self):
|
||||
from kvui import GameManager
|
||||
|
||||
@@ -162,6 +216,7 @@ async def game_watcher(ctx: FactorioContext):
|
||||
research_data = {int(tech_name.split("-")[1]) for tech_name in research_data}
|
||||
victory = data["victory"]
|
||||
await ctx.update_death_link(data["death_link"])
|
||||
ctx.multiplayer = data.get("multiplayer", False)
|
||||
|
||||
if not ctx.finished_game and victory:
|
||||
await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}])
|
||||
@@ -262,8 +317,17 @@ async def factorio_server_watcher(ctx: FactorioContext):
|
||||
if not ctx.awaiting_bridge and "Archipelago Bridge Data available for game tick " in msg:
|
||||
ctx.awaiting_bridge = True
|
||||
factorio_server_logger.debug(msg)
|
||||
elif re.match(r"^[0-9.]+ Script @[^ ]+\.lua:\d+: Player command toggle-ap-send-filter", msg):
|
||||
factorio_server_logger.debug(msg)
|
||||
ctx.toggle_filter_item_sends()
|
||||
elif re.match(r"^[0-9.]+ Script @[^ ]+\.lua:\d+: Player command toggle-ap-chat$", msg):
|
||||
factorio_server_logger.debug(msg)
|
||||
ctx.toggle_bridge_chat_out()
|
||||
else:
|
||||
factorio_server_logger.info(msg)
|
||||
match = re.match(r"^\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d \[CHAT\] ([^:]+): (.*)$", msg)
|
||||
if match:
|
||||
await ctx.chat_from_factorio(match.group(1), match.group(2))
|
||||
if ctx.rcon_client:
|
||||
commands = {}
|
||||
while ctx.send_index < len(ctx.items_received):
|
||||
@@ -363,6 +427,8 @@ async def factorio_spinup_server(ctx: FactorioContext) -> bool:
|
||||
|
||||
async def main(args):
|
||||
ctx = FactorioContext(args.connect, args.password)
|
||||
ctx.filter_item_sends = initial_filter_item_sends
|
||||
ctx.bridge_chat_out = initial_bridge_chat_out
|
||||
ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop")
|
||||
|
||||
if gui_enabled:
|
||||
@@ -415,6 +481,12 @@ if __name__ == '__main__':
|
||||
server_settings = args.server_settings if args.server_settings else options["factorio_options"].get("server_settings", None)
|
||||
if server_settings:
|
||||
server_settings = os.path.abspath(server_settings)
|
||||
if not isinstance(options["factorio_options"]["filter_item_sends"], bool):
|
||||
logging.warning(f"Warning: Option filter_item_sends should be a bool.")
|
||||
initial_filter_item_sends = bool(options["factorio_options"]["filter_item_sends"])
|
||||
if not isinstance(options["factorio_options"]["bridge_chat_out"], bool):
|
||||
logging.warning(f"Warning: Option bridge_chat_out should be a bool.")
|
||||
initial_bridge_chat_out = bool(options["factorio_options"]["bridge_chat_out"])
|
||||
|
||||
if not os.path.exists(os.path.dirname(executable)):
|
||||
raise FileNotFoundError(f"Path {os.path.dirname(executable)} does not exist or could not be accessed.")
|
||||
|
||||
39
Fill.py
39
Fill.py
@@ -258,6 +258,45 @@ def distribute_items_restrictive(world: MultiWorld) -> None:
|
||||
usefulitempool: typing.List[Item] = []
|
||||
filleritempool: typing.List[Item] = []
|
||||
|
||||
early_items_count: typing.Dict[typing.Tuple[str, int], int] = {}
|
||||
for player in world.player_ids:
|
||||
for item, count in world.early_items[player].value.items():
|
||||
early_items_count[(item, player)] = count
|
||||
if early_items_count:
|
||||
early_locations: typing.List[Location] = []
|
||||
early_priority_locations: typing.List[Location] = []
|
||||
for loc in reversed(fill_locations):
|
||||
if loc.can_reach(world.state):
|
||||
if loc.progress_type == LocationProgressType.PRIORITY:
|
||||
early_priority_locations.append(loc)
|
||||
else:
|
||||
early_locations.append(loc)
|
||||
fill_locations.remove(loc)
|
||||
|
||||
early_prog_items: typing.List[Item] = []
|
||||
early_rest_items: typing.List[Item] = []
|
||||
for item in reversed(itempool):
|
||||
if (item.name, item.player) in early_items_count:
|
||||
if item.advancement:
|
||||
early_prog_items.append(item)
|
||||
else:
|
||||
early_rest_items.append(item)
|
||||
itempool.remove(item)
|
||||
early_items_count[(item.name, item.player)] -= 1
|
||||
if early_items_count[(item.name, item.player)] == 0:
|
||||
del early_items_count[(item.name, item.player)]
|
||||
fill_restrictive(world, world.state, early_locations, early_rest_items, lock=True)
|
||||
early_locations += early_priority_locations
|
||||
fill_restrictive(world, world.state, early_locations, early_prog_items, lock=True)
|
||||
unplaced_early_items = early_rest_items + early_prog_items
|
||||
if unplaced_early_items:
|
||||
logging.warning(f"Ran out of early locations for early items. Failed to place \
|
||||
{len(unplaced_early_items)} items early.")
|
||||
itempool += unplaced_early_items
|
||||
|
||||
fill_locations += early_locations + early_priority_locations
|
||||
world.random.shuffle(fill_locations)
|
||||
|
||||
for item in itempool:
|
||||
if item.advancement:
|
||||
progitempool.append(item)
|
||||
|
||||
@@ -26,7 +26,9 @@ ModuleUpdate.update()
|
||||
from worlds.alttp.Rom import Sprite, LocalRom, apply_rom_settings, get_base_rom_bytes
|
||||
from Utils import output_path, local_path, user_path, open_file, get_cert_none_ssl_context, persistent_store, \
|
||||
get_adjuster_settings, tkinter_center_window, init_logging
|
||||
from Patch import GAME_ALTTP
|
||||
|
||||
|
||||
GAME_ALTTP = "A Link to the Past"
|
||||
|
||||
|
||||
class AdjusterWorld(object):
|
||||
|
||||
29
Main.py
29
Main.py
@@ -80,15 +80,30 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
||||
|
||||
logger.info("Found World Types:")
|
||||
longest_name = max(len(text) for text in AutoWorld.AutoWorldRegister.world_types)
|
||||
numlength = 8
|
||||
|
||||
max_item = 0
|
||||
max_location = 0
|
||||
for cls in AutoWorld.AutoWorldRegister.world_types.values():
|
||||
if cls.item_id_to_name:
|
||||
max_item = max(max_item, max(cls.item_id_to_name))
|
||||
max_location = max(max_location, max(cls.location_id_to_name))
|
||||
|
||||
item_digits = len(str(max_item))
|
||||
location_digits = len(str(max_location))
|
||||
item_count = len(str(max(len(cls.item_names) for cls in AutoWorld.AutoWorldRegister.world_types.values())))
|
||||
location_count = len(str(max(len(cls.location_names) for cls in AutoWorld.AutoWorldRegister.world_types.values())))
|
||||
del max_item, max_location
|
||||
|
||||
for name, cls in AutoWorld.AutoWorldRegister.world_types.items():
|
||||
if not cls.hidden and len(cls.item_names) > 0:
|
||||
logger.info(f" {name:{longest_name}}: {len(cls.item_names):3} "
|
||||
f"Items (IDs: {min(cls.item_id_to_name):{numlength}} - "
|
||||
f"{max(cls.item_id_to_name):{numlength}}) | "
|
||||
f"{len(cls.location_names):3} "
|
||||
f"Locations (IDs: {min(cls.location_id_to_name):{numlength}} - "
|
||||
f"{max(cls.location_id_to_name):{numlength}})")
|
||||
logger.info(f" {name:{longest_name}}: {len(cls.item_names):{item_count}} "
|
||||
f"Items (IDs: {min(cls.item_id_to_name):{item_digits}} - "
|
||||
f"{max(cls.item_id_to_name):{item_digits}}) | "
|
||||
f"{len(cls.location_names):{location_count}} "
|
||||
f"Locations (IDs: {min(cls.location_id_to_name):{location_digits}} - "
|
||||
f"{max(cls.location_id_to_name):{location_digits}})")
|
||||
|
||||
del item_digits, location_digits, item_count, location_count
|
||||
|
||||
AutoWorld.call_stage(world, "assert_generate")
|
||||
|
||||
|
||||
@@ -998,7 +998,11 @@ class CommandMeta(type):
|
||||
return super(CommandMeta, cls).__new__(cls, name, bases, attrs)
|
||||
|
||||
|
||||
def mark_raw(function):
|
||||
_Return = typing.TypeVar("_Return")
|
||||
# TODO: when python 3.10 is lowest supported, typing.ParamSpec
|
||||
|
||||
|
||||
def mark_raw(function: typing.Callable[[typing.Any], _Return]) -> typing.Callable[[typing.Any], _Return]:
|
||||
function.raw_text = True
|
||||
return function
|
||||
|
||||
@@ -1328,6 +1332,8 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
||||
|
||||
def get_hints(self, input_text: str, for_location: bool = False) -> bool:
|
||||
points_available = get_client_points(self.ctx, self.client)
|
||||
cost = self.ctx.get_hint_cost(self.client.slot)
|
||||
|
||||
if not input_text:
|
||||
hints = {hint.re_check(self.ctx, self.client.team) for hint in
|
||||
self.ctx.hints[self.client.team, self.client.slot]}
|
||||
@@ -1382,7 +1388,6 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
||||
return False
|
||||
|
||||
if hints:
|
||||
cost = self.ctx.get_hint_cost(self.client.slot)
|
||||
new_hints = set(hints) - self.ctx.hints[self.client.team, self.client.slot]
|
||||
old_hints = set(hints) - new_hints
|
||||
if old_hints:
|
||||
@@ -1432,7 +1437,12 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
||||
return True
|
||||
|
||||
else:
|
||||
self.output("Nothing found. Item/Location may not exist.")
|
||||
if points_available >= cost:
|
||||
self.output("Nothing found. Item/Location may not exist.")
|
||||
else:
|
||||
self.output(f"You can't afford the hint. "
|
||||
f"You have {points_available} points and need at least "
|
||||
f"{self.ctx.get_hint_cost(self.client.slot)}.")
|
||||
return False
|
||||
|
||||
@mark_raw
|
||||
|
||||
17
Options.py
17
Options.py
@@ -1,5 +1,6 @@
|
||||
from __future__ import annotations
|
||||
import abc
|
||||
from copy import deepcopy
|
||||
import math
|
||||
import numbers
|
||||
import typing
|
||||
@@ -753,7 +754,7 @@ class OptionDict(Option[typing.Dict[str, typing.Any]], VerifyKeys):
|
||||
supports_weighting = False
|
||||
|
||||
def __init__(self, value: typing.Dict[str, typing.Any]):
|
||||
self.value = value
|
||||
self.value = deepcopy(value)
|
||||
|
||||
@classmethod
|
||||
def from_any(cls, data: typing.Dict[str, typing.Any]) -> OptionDict:
|
||||
@@ -784,7 +785,7 @@ class OptionList(Option[typing.List[typing.Any]], VerifyKeys):
|
||||
supports_weighting = False
|
||||
|
||||
def __init__(self, value: typing.List[typing.Any]):
|
||||
self.value = value or []
|
||||
self.value = deepcopy(value)
|
||||
super(OptionList, self).__init__()
|
||||
|
||||
@classmethod
|
||||
@@ -806,11 +807,11 @@ class OptionList(Option[typing.List[typing.Any]], VerifyKeys):
|
||||
|
||||
|
||||
class OptionSet(Option[typing.Set[str]], VerifyKeys):
|
||||
default = frozenset()
|
||||
default: typing.Union[typing.Set[str], typing.FrozenSet[str]] = frozenset()
|
||||
supports_weighting = False
|
||||
|
||||
def __init__(self, value: typing.Union[typing.Set[str, typing.Any], typing.List[str, typing.Any]]):
|
||||
self.value = set(value)
|
||||
def __init__(self, value: typing.Iterable[str]):
|
||||
self.value = set(deepcopy(value))
|
||||
super(OptionSet, self).__init__()
|
||||
|
||||
@classmethod
|
||||
@@ -882,6 +883,11 @@ class NonLocalItems(ItemSet):
|
||||
display_name = "Not Local Items"
|
||||
|
||||
|
||||
class EarlyItems(ItemDict):
|
||||
"""Force the specified items to be in locations that are reachable from the start."""
|
||||
display_name = "Early Items"
|
||||
|
||||
|
||||
class StartInventory(ItemDict):
|
||||
"""Start with these items."""
|
||||
verify_item_name = True
|
||||
@@ -980,6 +986,7 @@ per_game_common_options = {
|
||||
**common_options, # can be overwritten per-game
|
||||
"local_items": LocalItems,
|
||||
"non_local_items": NonLocalItems,
|
||||
"early_items": EarlyItems,
|
||||
"start_inventory": StartInventory,
|
||||
"start_hints": StartHints,
|
||||
"start_location_hints": StartLocationHints,
|
||||
|
||||
10
Patch.py
10
Patch.py
@@ -11,16 +11,6 @@ if __name__ == "__main__":
|
||||
from worlds.Files import AutoPatchRegister, APDeltaPatch
|
||||
|
||||
|
||||
GAME_ALTTP = "A Link to the Past"
|
||||
GAME_SM = "Super Metroid"
|
||||
GAME_SOE = "Secret of Evermore"
|
||||
GAME_SMZ3 = "SMZ3"
|
||||
GAME_DKC3 = "Donkey Kong Country 3"
|
||||
|
||||
GAME_SMW = "Super Mario World"
|
||||
|
||||
|
||||
|
||||
class RomMeta(TypedDict):
|
||||
server: str
|
||||
player: Optional[int]
|
||||
|
||||
1129
SNIClient.py
1129
SNIClient.py
File diff suppressed because it is too large
Load Diff
@@ -155,7 +155,9 @@ class SC2Context(CommonContext):
|
||||
items_handling = 0b111
|
||||
difficulty = -1
|
||||
all_in_choice = 0
|
||||
mission_order = 0
|
||||
mission_req_table: typing.Dict[str, MissionInfo] = {}
|
||||
final_mission: int = 29
|
||||
announcements = queue.Queue()
|
||||
sc2_run_task: typing.Optional[asyncio.Task] = None
|
||||
missions_unlocked: bool = False # allow launching missions ignoring requirements
|
||||
@@ -180,9 +182,15 @@ class SC2Context(CommonContext):
|
||||
self.difficulty = args["slot_data"]["game_difficulty"]
|
||||
self.all_in_choice = args["slot_data"]["all_in_map"]
|
||||
slot_req_table = args["slot_data"]["mission_req"]
|
||||
# Maintaining backwards compatibility with older slot data
|
||||
self.mission_req_table = {
|
||||
mission: MissionInfo(**slot_req_table[mission]) for mission in slot_req_table
|
||||
mission: MissionInfo(
|
||||
**{field: value for field, value in mission_info.items() if field in MissionInfo._fields}
|
||||
)
|
||||
for mission, mission_info in slot_req_table.items()
|
||||
}
|
||||
self.mission_order = args["slot_data"].get("mission_order", 0)
|
||||
self.final_mission = args["slot_data"].get("final_mission", 29)
|
||||
|
||||
self.build_location_to_mission_mapping()
|
||||
|
||||
@@ -304,7 +312,6 @@ class SC2Context(CommonContext):
|
||||
self.refresh_from_launching = True
|
||||
|
||||
self.mission_panel.clear_widgets()
|
||||
|
||||
if self.ctx.mission_req_table:
|
||||
self.last_checked_locations = self.ctx.checked_locations.copy()
|
||||
self.first_check = False
|
||||
@@ -322,17 +329,20 @@ class SC2Context(CommonContext):
|
||||
|
||||
for category in categories:
|
||||
category_panel = MissionCategory()
|
||||
if category.startswith('_'):
|
||||
category_display_name = ''
|
||||
else:
|
||||
category_display_name = category
|
||||
category_panel.add_widget(
|
||||
Label(text=category, size_hint_y=None, height=50, outline_width=1))
|
||||
Label(text=category_display_name, size_hint_y=None, height=50, outline_width=1))
|
||||
|
||||
for mission in categories[category]:
|
||||
text: str = mission
|
||||
tooltip: str = ""
|
||||
|
||||
mission_id: int = self.ctx.mission_req_table[mission].id
|
||||
# Map has uncollected locations
|
||||
if mission in unfinished_missions:
|
||||
text = f"[color=6495ED]{text}[/color]"
|
||||
|
||||
elif mission in available_missions:
|
||||
text = f"[color=FFFFFF]{text}[/color]"
|
||||
# Map requirements not met
|
||||
@@ -351,6 +361,16 @@ class SC2Context(CommonContext):
|
||||
remaining_location_names: typing.List[str] = [
|
||||
self.ctx.location_names[loc] for loc in self.ctx.locations_for_mission(mission)
|
||||
if loc in self.ctx.missing_locations]
|
||||
|
||||
if mission_id == self.ctx.final_mission:
|
||||
if mission in available_missions:
|
||||
text = f"[color=FFBC95]{mission}[/color]"
|
||||
else:
|
||||
text = f"[color=D0C0BE]{mission}[/color]"
|
||||
if tooltip:
|
||||
tooltip += "\n"
|
||||
tooltip += "Final Mission"
|
||||
|
||||
if remaining_location_names:
|
||||
if tooltip:
|
||||
tooltip += "\n"
|
||||
@@ -360,7 +380,7 @@ class SC2Context(CommonContext):
|
||||
mission_button = MissionButton(text=text, size_hint_y=None, height=50)
|
||||
mission_button.tooltip_text = tooltip
|
||||
mission_button.bind(on_press=self.mission_callback)
|
||||
self.mission_id_to_button[self.ctx.mission_req_table[mission].id] = mission_button
|
||||
self.mission_id_to_button[mission_id] = mission_button
|
||||
category_panel.add_widget(mission_button)
|
||||
|
||||
category_panel.add_widget(Label(text=""))
|
||||
@@ -469,6 +489,9 @@ wol_default_categories = [
|
||||
"Rebellion", "Rebellion", "Rebellion", "Rebellion", "Rebellion", "Prophecy", "Prophecy", "Prophecy", "Prophecy",
|
||||
"Char", "Char", "Char", "Char"
|
||||
]
|
||||
wol_default_category_names = [
|
||||
"Mar Sara", "Colonist", "Artifact", "Covert", "Rebellion", "Prophecy", "Char"
|
||||
]
|
||||
|
||||
|
||||
def calculate_items(items: typing.List[NetworkItem]) -> typing.List[int]:
|
||||
@@ -586,7 +609,7 @@ class ArchipelagoBot(sc2.bot_ai.BotAI):
|
||||
|
||||
if self.can_read_game:
|
||||
if game_state & (1 << 1) and not self.mission_completed:
|
||||
if self.mission_id != 29:
|
||||
if self.mission_id != self.ctx.final_mission:
|
||||
print("Mission Completed")
|
||||
await self.ctx.send_msgs(
|
||||
[{"cmd": 'LocationChecks',
|
||||
@@ -742,13 +765,14 @@ def calc_available_missions(ctx: SC2Context, unlocks=None):
|
||||
return available_missions
|
||||
|
||||
|
||||
def mission_reqs_completed(ctx: SC2Context, mission_name: str, missions_complete):
|
||||
def mission_reqs_completed(ctx: SC2Context, mission_name: str, missions_complete: int):
|
||||
"""Returns a bool signifying if the mission has all requirements complete and can be done
|
||||
|
||||
Arguments:
|
||||
ctx -- instance of SC2Context
|
||||
locations_to_check -- the mission string name to check
|
||||
missions_complete -- an int of how many missions have been completed
|
||||
mission_path -- a list of missions that have already been checked
|
||||
"""
|
||||
if len(ctx.mission_req_table[mission_name].required_world) >= 1:
|
||||
# A check for when the requirements are being or'd
|
||||
@@ -766,7 +790,18 @@ def mission_reqs_completed(ctx: SC2Context, mission_name: str, missions_complete
|
||||
else:
|
||||
req_success = False
|
||||
|
||||
# Grid-specific logic (to avoid long path checks and infinite recursion)
|
||||
if ctx.mission_order in (3, 4):
|
||||
if req_success:
|
||||
return True
|
||||
else:
|
||||
if req_mission is ctx.mission_req_table[mission_name].required_world[-1]:
|
||||
return False
|
||||
else:
|
||||
continue
|
||||
|
||||
# Recursively check required mission to see if it's requirements are met, in case !collect has been done
|
||||
# Skipping recursive check on Grid settings to speed up checks and avoid infinite recursion
|
||||
if not mission_reqs_completed(ctx, list(ctx.mission_req_table)[req_mission - 1], missions_complete):
|
||||
if not ctx.mission_req_table[mission_name].or_requirements:
|
||||
return False
|
||||
|
||||
17
Utils.py
17
Utils.py
@@ -141,7 +141,7 @@ def user_path(*path: str) -> str:
|
||||
return os.path.join(user_path.cached_path, *path)
|
||||
|
||||
|
||||
def output_path(*path: str):
|
||||
def output_path(*path: str) -> str:
|
||||
if hasattr(output_path, 'cached_path'):
|
||||
return os.path.join(output_path.cached_path, *path)
|
||||
output_path.cached_path = user_path(get_options()["general_options"]["output_path"])
|
||||
@@ -231,20 +231,21 @@ def get_default_options() -> OptionsType:
|
||||
},
|
||||
"factorio_options": {
|
||||
"executable": os.path.join("factorio", "bin", "x64", "factorio"),
|
||||
"filter_item_sends": False,
|
||||
"bridge_chat_out": True,
|
||||
},
|
||||
"sni_options": {
|
||||
"sni": "SNI",
|
||||
"snes_rom_start": True,
|
||||
},
|
||||
"sm_options": {
|
||||
"rom_file": "Super Metroid (JU).sfc",
|
||||
"sni": "SNI",
|
||||
"rom_start": True,
|
||||
},
|
||||
"soe_options": {
|
||||
"rom_file": "Secret of Evermore (USA).sfc",
|
||||
},
|
||||
"lttp_options": {
|
||||
"rom_file": "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc",
|
||||
"sni": "SNI",
|
||||
"rom_start": True,
|
||||
|
||||
},
|
||||
"server_options": {
|
||||
"host": None,
|
||||
@@ -287,13 +288,9 @@ def get_default_options() -> OptionsType:
|
||||
},
|
||||
"dkc3_options": {
|
||||
"rom_file": "Donkey Kong Country 3 - Dixie Kong's Double Trouble! (USA) (En,Fr).sfc",
|
||||
"sni": "SNI",
|
||||
"rom_start": True,
|
||||
},
|
||||
"smw_options": {
|
||||
"rom_file": "Super Mario World (USA).sfc",
|
||||
"sni": "SNI",
|
||||
"rom_start": True,
|
||||
},
|
||||
"zillion_options": {
|
||||
"rom_file": "Zillion (UE) [!].sms",
|
||||
|
||||
190
ZillionClient.py
190
ZillionClient.py
@@ -1,7 +1,7 @@
|
||||
import asyncio
|
||||
import base64
|
||||
import platform
|
||||
from typing import Any, Coroutine, Dict, Optional, Type, cast
|
||||
from typing import Any, ClassVar, Coroutine, Dict, List, Optional, Protocol, Tuple, Type, cast
|
||||
|
||||
# CommonClient import first to trigger ModuleUpdater
|
||||
from CommonClient import CommonContext, server_loop, gui_enabled, \
|
||||
@@ -18,7 +18,7 @@ from zilliandomizer.options import Chars
|
||||
from zilliandomizer.patch import RescueInfo
|
||||
|
||||
from worlds.zillion.id_maps import make_id_to_others
|
||||
from worlds.zillion.config import base_id
|
||||
from worlds.zillion.config import base_id, zillion_map
|
||||
|
||||
|
||||
class ZillionCommandProcessor(ClientCommandProcessor):
|
||||
@@ -29,6 +29,18 @@ class ZillionCommandProcessor(ClientCommandProcessor):
|
||||
logger.info("ready to look for game")
|
||||
self.ctx.look_for_retroarch.set()
|
||||
|
||||
def _cmd_map(self) -> None:
|
||||
""" Toggle view of the map tracker. """
|
||||
self.ctx.ui_toggle_map()
|
||||
|
||||
|
||||
class ToggleCallback(Protocol):
|
||||
def __call__(self) -> None: ...
|
||||
|
||||
|
||||
class SetRoomCallback(Protocol):
|
||||
def __call__(self, rooms: List[List[int]]) -> None: ...
|
||||
|
||||
|
||||
class ZillionContext(CommonContext):
|
||||
game = "Zillion"
|
||||
@@ -46,6 +58,8 @@ class ZillionContext(CommonContext):
|
||||
start_char: Chars = "JJ"
|
||||
rescues: Dict[int, RescueInfo] = {}
|
||||
loc_mem_to_id: Dict[int, int] = {}
|
||||
got_room_info: asyncio.Event
|
||||
""" flag for connected to server """
|
||||
got_slot_data: asyncio.Event
|
||||
""" serves as a flag for whether I am logged in to the server """
|
||||
|
||||
@@ -59,13 +73,20 @@ class ZillionContext(CommonContext):
|
||||
As a workaround, we don't look for RetroArch until this event is set.
|
||||
"""
|
||||
|
||||
ui_toggle_map: ToggleCallback
|
||||
ui_set_rooms: SetRoomCallback
|
||||
""" parameter is y 16 x 8 numbers to show in each room """
|
||||
|
||||
def __init__(self,
|
||||
server_address: str,
|
||||
password: str) -> None:
|
||||
super().__init__(server_address, password)
|
||||
self.from_game = asyncio.Queue()
|
||||
self.to_game = asyncio.Queue()
|
||||
self.got_room_info = asyncio.Event()
|
||||
self.got_slot_data = asyncio.Event()
|
||||
self.ui_toggle_map = lambda: None
|
||||
self.ui_set_rooms = lambda rooms: None
|
||||
|
||||
self.look_for_retroarch = asyncio.Event()
|
||||
if platform.system() != "Windows":
|
||||
@@ -112,6 +133,10 @@ class ZillionContext(CommonContext):
|
||||
# override
|
||||
def run_gui(self) -> None:
|
||||
from kvui import GameManager
|
||||
from kivy.core.text import Label as CoreLabel
|
||||
from kivy.graphics import Ellipse, Color, Rectangle
|
||||
from kivy.uix.layout import Layout
|
||||
from kivy.uix.widget import Widget
|
||||
|
||||
class ZillionManager(GameManager):
|
||||
logging_pairs = [
|
||||
@@ -119,12 +144,76 @@ class ZillionContext(CommonContext):
|
||||
]
|
||||
base_title = "Archipelago Zillion Client"
|
||||
|
||||
class MapPanel(Widget):
|
||||
MAP_WIDTH: ClassVar[int] = 281
|
||||
|
||||
_number_textures: List[Any] = []
|
||||
rooms: List[List[int]] = []
|
||||
|
||||
def __init__(self, **kwargs: Any) -> None:
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self.rooms = [[0 for _ in range(8)] for _ in range(16)]
|
||||
|
||||
self._make_numbers()
|
||||
self.update_map()
|
||||
|
||||
self.bind(pos=self.update_map)
|
||||
# self.bind(size=self.update_bg)
|
||||
|
||||
def _make_numbers(self) -> None:
|
||||
self._number_textures = []
|
||||
for n in range(10):
|
||||
label = CoreLabel(text=str(n), font_size=22, color=(0.1, 0.9, 0, 1))
|
||||
label.refresh()
|
||||
self._number_textures.append(label.texture)
|
||||
|
||||
def update_map(self, *args: Any) -> None:
|
||||
self.canvas.clear()
|
||||
|
||||
with self.canvas:
|
||||
Color(1, 1, 1, 1)
|
||||
Rectangle(source=zillion_map,
|
||||
pos=self.pos,
|
||||
size=(ZillionManager.MapPanel.MAP_WIDTH,
|
||||
int(ZillionManager.MapPanel.MAP_WIDTH * 1.456))) # aspect ratio of that image
|
||||
for y in range(16):
|
||||
for x in range(8):
|
||||
num = self.rooms[15 - y][x]
|
||||
if num > 0:
|
||||
Color(0, 0, 0, 0.4)
|
||||
pos = [self.pos[0] + 17 + x * 32, self.pos[1] + 14 + y * 24]
|
||||
Ellipse(size=[22, 22], pos=pos)
|
||||
Color(1, 1, 1, 1)
|
||||
pos = [self.pos[0] + 22 + x * 32, self.pos[1] + 12 + y * 24]
|
||||
num_texture = self._number_textures[num]
|
||||
Rectangle(texture=num_texture, size=num_texture.size, pos=pos)
|
||||
|
||||
def build(self) -> Layout:
|
||||
container = super().build()
|
||||
self.map_widget = ZillionManager.MapPanel(size_hint_x=None, width=0)
|
||||
self.main_area_container.add_widget(self.map_widget)
|
||||
return container
|
||||
|
||||
def toggle_map_width(self) -> None:
|
||||
if self.map_widget.width == 0:
|
||||
self.map_widget.width = ZillionManager.MapPanel.MAP_WIDTH
|
||||
else:
|
||||
self.map_widget.width = 0
|
||||
self.container.do_layout()
|
||||
|
||||
def set_rooms(self, rooms: List[List[int]]) -> None:
|
||||
self.map_widget.rooms = rooms
|
||||
self.map_widget.update_map()
|
||||
|
||||
self.ui = ZillionManager(self)
|
||||
run_co: Coroutine[Any, Any, None] = self.ui.async_run() # type: ignore
|
||||
# kivy types missing
|
||||
self.ui_toggle_map = lambda: self.ui.toggle_map_width()
|
||||
self.ui_set_rooms = lambda rooms: self.ui.set_rooms(rooms)
|
||||
run_co: Coroutine[Any, Any, None] = self.ui.async_run()
|
||||
self.ui_task = asyncio.create_task(run_co, name="UI")
|
||||
|
||||
def on_package(self, cmd: str, args: Dict[str, Any]) -> None:
|
||||
self.room_item_numbers_to_ui()
|
||||
if cmd == "Connected":
|
||||
logger.info("logged in to Archipelago server")
|
||||
if "slot_data" not in args:
|
||||
@@ -185,6 +274,24 @@ class ZillionContext(CommonContext):
|
||||
logger.info("received door data from server")
|
||||
doors = base64.b64decode(doors_b64)
|
||||
self.to_game.put_nowait(events.DoorEventToGame(doors))
|
||||
elif cmd == "RoomInfo":
|
||||
self.seed_name = args["seed_name"]
|
||||
self.got_room_info.set()
|
||||
|
||||
def room_item_numbers_to_ui(self) -> None:
|
||||
rooms = [[0 for _ in range(8)] for _ in range(16)]
|
||||
for loc_id in self.missing_locations:
|
||||
loc_id_small = loc_id - base_id
|
||||
loc_name = id_to_loc[loc_id_small]
|
||||
y = ord(loc_name[0]) - 65
|
||||
x = ord(loc_name[2]) - 49
|
||||
if y == 9 and x == 5:
|
||||
# don't show main computer in numbers
|
||||
continue
|
||||
assert (0 <= y < 16) and (0 <= x < 8), f"invalid index from location name {loc_name}"
|
||||
rooms[y][x] += 1
|
||||
# TODO: also add locations with locals lost from loading save state or reset
|
||||
self.ui_set_rooms(rooms)
|
||||
|
||||
def process_from_game_queue(self) -> None:
|
||||
if self.from_game.qsize():
|
||||
@@ -238,6 +345,24 @@ class ZillionContext(CommonContext):
|
||||
self.next_item = len(self.items_received)
|
||||
|
||||
|
||||
def name_seed_from_ram(data: bytes) -> Tuple[str, str]:
|
||||
""" returns player name, and end of seed string """
|
||||
if len(data) == 0:
|
||||
# no connection to game
|
||||
return "", "xxx"
|
||||
null_index = data.find(b'\x00')
|
||||
if null_index == -1:
|
||||
logger.warning(f"invalid game id in rom {repr(data)}")
|
||||
null_index = len(data)
|
||||
name = data[:null_index].decode()
|
||||
null_index_2 = data.find(b'\x00', null_index + 1)
|
||||
if null_index_2 == -1:
|
||||
null_index_2 = len(data)
|
||||
seed_name = data[null_index + 1:null_index_2].decode()
|
||||
|
||||
return name, seed_name
|
||||
|
||||
|
||||
async def zillion_sync_task(ctx: ZillionContext) -> None:
|
||||
logger.info("started zillion sync task")
|
||||
|
||||
@@ -263,47 +388,58 @@ async def zillion_sync_task(ctx: ZillionContext) -> None:
|
||||
with Memory(ctx.from_game, ctx.to_game) as memory:
|
||||
while not ctx.exit_event.is_set():
|
||||
ram = await memory.read()
|
||||
name = memory.get_player_name(ram).decode()
|
||||
game_id = memory.get_rom_to_ram_data(ram)
|
||||
name, seed_end = name_seed_from_ram(game_id)
|
||||
if len(name):
|
||||
if name == ctx.auth:
|
||||
# this is the name we know
|
||||
if ctx.server and ctx.server.socket: # type: ignore
|
||||
if memory.have_generation_info():
|
||||
log_no_spam("everything connected")
|
||||
await memory.process_ram(ram)
|
||||
ctx.process_from_game_queue()
|
||||
ctx.process_items_received()
|
||||
else: # no generation info
|
||||
if ctx.got_slot_data.is_set():
|
||||
memory.set_generation_info(ctx.rescues, ctx.loc_mem_to_id)
|
||||
ctx.ap_id_to_name, ctx.ap_id_to_zz_id, _ap_id_to_zz_item = \
|
||||
make_id_to_others(ctx.start_char)
|
||||
ctx.next_item = 0
|
||||
ctx.ap_local_count = len(ctx.checked_locations)
|
||||
else: # no slot data yet
|
||||
asyncio.create_task(ctx.send_connect())
|
||||
log_no_spam("logging in to server...")
|
||||
await asyncio.wait((
|
||||
ctx.got_slot_data.wait(),
|
||||
ctx.exit_event.wait(),
|
||||
asyncio.sleep(6)
|
||||
), return_when=asyncio.FIRST_COMPLETED) # to not spam connect packets
|
||||
if ctx.got_room_info.is_set():
|
||||
if ctx.seed_name and ctx.seed_name.endswith(seed_end):
|
||||
# correct seed
|
||||
if memory.have_generation_info():
|
||||
log_no_spam("everything connected")
|
||||
await memory.process_ram(ram)
|
||||
ctx.process_from_game_queue()
|
||||
ctx.process_items_received()
|
||||
else: # no generation info
|
||||
if ctx.got_slot_data.is_set():
|
||||
memory.set_generation_info(ctx.rescues, ctx.loc_mem_to_id)
|
||||
ctx.ap_id_to_name, ctx.ap_id_to_zz_id, _ap_id_to_zz_item = \
|
||||
make_id_to_others(ctx.start_char)
|
||||
ctx.next_item = 0
|
||||
ctx.ap_local_count = len(ctx.checked_locations)
|
||||
else: # no slot data yet
|
||||
asyncio.create_task(ctx.send_connect())
|
||||
log_no_spam("logging in to server...")
|
||||
await asyncio.wait((
|
||||
ctx.got_slot_data.wait(),
|
||||
ctx.exit_event.wait(),
|
||||
asyncio.sleep(6)
|
||||
), return_when=asyncio.FIRST_COMPLETED) # to not spam connect packets
|
||||
else: # not correct seed name
|
||||
log_no_spam("incorrect seed - did you mix up roms?")
|
||||
else: # no room info
|
||||
# If we get here, it looks like `RoomInfo` packet got lost
|
||||
log_no_spam("waiting for room info from server...")
|
||||
else: # server not connected
|
||||
log_no_spam("waiting for server connection...")
|
||||
else: # new game
|
||||
log_no_spam("connected to new game")
|
||||
await ctx.disconnect()
|
||||
ctx.reset_server_state()
|
||||
ctx.seed_name = None
|
||||
ctx.got_room_info.clear()
|
||||
ctx.reset_game_state()
|
||||
memory.reset_game_state()
|
||||
|
||||
ctx.auth = name
|
||||
asyncio.create_task(ctx.connect())
|
||||
await asyncio.wait((
|
||||
ctx.got_slot_data.wait(),
|
||||
ctx.got_room_info.wait(),
|
||||
ctx.exit_event.wait(),
|
||||
asyncio.sleep(6)
|
||||
), return_when=asyncio.FIRST_COMPLETED) # to not spam connect packets
|
||||
), return_when=asyncio.FIRST_COMPLETED)
|
||||
else: # no name found in game
|
||||
if not help_message_shown:
|
||||
logger.info('In RetroArch, make sure "Settings > Network > Network Commands" is on.')
|
||||
|
||||
48
host.yaml
48
host.yaml
@@ -82,28 +82,27 @@ generator:
|
||||
# List of options that can be plando'd. Can be combined, for example "bosses, items"
|
||||
# Available options: bosses, items, texts, connections
|
||||
plando_options: "bosses"
|
||||
sni_options:
|
||||
# Set this to your SNI folder location if you want the MultiClient to attempt an auto start, does nothing if not found
|
||||
sni_path: "SNI"
|
||||
# Set this to false to never autostart a rom (such as after patching)
|
||||
# True for operating system default program
|
||||
# Alternatively, a path to a program to open the .sfc file with
|
||||
snes_rom_start: true
|
||||
lttp_options:
|
||||
# File name of the v1.0 J rom
|
||||
rom_file: "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc"
|
||||
# Set this to your SNI folder location if you want the MultiClient to attempt an auto start, does nothing if not found
|
||||
sni: "SNI"
|
||||
# Set this to false to never autostart a rom (such as after patching)
|
||||
# True for operating system default program
|
||||
# Alternatively, a path to a program to open the .sfc file with
|
||||
rom_start: true
|
||||
sm_options:
|
||||
# File name of the v1.0 J rom
|
||||
rom_file: "Super Metroid (JU).sfc"
|
||||
# Set this to your SNI folder location if you want the MultiClient to attempt an auto start, does nothing if not found
|
||||
sni: "SNI"
|
||||
# Set this to false to never autostart a rom (such as after patching)
|
||||
# True for operating system default program
|
||||
# Alternatively, a path to a program to open the .sfc file with
|
||||
rom_start: true
|
||||
factorio_options:
|
||||
executable: "factorio/bin/x64/factorio"
|
||||
# by default, no settings are loaded if this file does not exist. If this file does exist, then it will be used.
|
||||
# server_settings: "factorio\\data\\server-settings.json"
|
||||
# Whether to filter item send messages displayed in-game to only those that involve you.
|
||||
filter_item_sends: false
|
||||
# Whether to send chat messages from players on the Factorio server to Archipelago.
|
||||
bridge_chat_out: true
|
||||
minecraft_options:
|
||||
forge_directory: "Minecraft Forge server"
|
||||
max_heap_size: "2G"
|
||||
@@ -122,22 +121,12 @@ soe_options:
|
||||
rom_file: "Secret of Evermore (USA).sfc"
|
||||
ffr_options:
|
||||
display_msgs: true
|
||||
smz3_options:
|
||||
# Set this to your SNI folder location if you want the MultiClient to attempt an auto start, does nothing if not found
|
||||
sni: "SNI"
|
||||
# Set this to false to never autostart a rom (such as after patching)
|
||||
# True for operating system default program
|
||||
# Alternatively, a path to a program to open the .sfc file with
|
||||
rom_start: true
|
||||
dkc3_options:
|
||||
# File name of the DKC3 US rom
|
||||
rom_file: "Donkey Kong Country 3 - Dixie Kong's Double Trouble! (USA) (En,Fr).sfc"
|
||||
# Set this to your SNI folder location if you want the MultiClient to attempt an auto start, does nothing if not found
|
||||
sni: "SNI"
|
||||
# Set this to false to never autostart a rom (such as after patching)
|
||||
# True for operating system default program
|
||||
# Alternatively, a path to a program to open the .sfc file with
|
||||
rom_start: true
|
||||
smw_options:
|
||||
# File name of the SMW US rom
|
||||
rom_file: "Super Mario World (USA).sfc"
|
||||
pokemon_rb_options:
|
||||
# File names of the Pokemon Red and Blue roms
|
||||
red_rom_file: "Pokemon Red (UE) [S][!].gb"
|
||||
@@ -146,15 +135,6 @@ pokemon_rb_options:
|
||||
# True for operating system default program
|
||||
# Alternatively, a path to a program to open the .gb file with
|
||||
rom_start: true
|
||||
smw_options:
|
||||
# File name of the SMW US rom
|
||||
rom_file: "Super Mario World (USA).sfc"
|
||||
# Set this to your SNI folder location if you want the MultiClient to attempt an auto start, does nothing if not found
|
||||
sni: "SNI"
|
||||
# Set this to false to never autostart a rom (such as after patching)
|
||||
# True for operating system default program
|
||||
# Alternatively, a path to a program to open the .sfc file with
|
||||
rom_start: true
|
||||
zillion_options:
|
||||
# File name of the Zillion US rom
|
||||
rom_file: "Zillion (UE) [!].sms"
|
||||
|
||||
11
kvui.py
11
kvui.py
@@ -28,6 +28,7 @@ from kivy.factory import Factory
|
||||
from kivy.properties import BooleanProperty, ObjectProperty
|
||||
from kivy.uix.button import Button
|
||||
from kivy.uix.gridlayout import GridLayout
|
||||
from kivy.uix.layout import Layout
|
||||
from kivy.uix.textinput import TextInput
|
||||
from kivy.uix.recycleview import RecycleView
|
||||
from kivy.uix.tabbedpanel import TabbedPanel, TabbedPanelItem
|
||||
@@ -299,6 +300,9 @@ class GameManager(App):
|
||||
base_title: str = "Archipelago Client"
|
||||
last_autofillable_command: str
|
||||
|
||||
main_area_container: GridLayout
|
||||
""" subclasses can add more columns beside the tabs """
|
||||
|
||||
def __init__(self, ctx: context_type):
|
||||
self.title = self.base_title
|
||||
self.ctx = ctx
|
||||
@@ -325,7 +329,7 @@ class GameManager(App):
|
||||
|
||||
super(GameManager, self).__init__()
|
||||
|
||||
def build(self):
|
||||
def build(self) -> Layout:
|
||||
self.container = ContainerLayout()
|
||||
|
||||
self.grid = MainLayout()
|
||||
@@ -358,7 +362,10 @@ class GameManager(App):
|
||||
self.log_panels[display_name] = panel.content = UILog(bridge_logger)
|
||||
self.tabs.add_widget(panel)
|
||||
|
||||
self.grid.add_widget(self.tabs)
|
||||
self.main_area_container = GridLayout(size_hint_y=1, rows=1)
|
||||
self.main_area_container.add_widget(self.tabs)
|
||||
|
||||
self.grid.add_widget(self.main_area_container)
|
||||
|
||||
if len(self.logging_pairs) == 1:
|
||||
# Hide Tab selection if only one tab
|
||||
|
||||
@@ -312,9 +312,10 @@ class TestAdvancements(TestMinecraft):
|
||||
["Two by Two", False, [], ['Flint and Steel']],
|
||||
["Two by Two", False, [], ['Progressive Tools']],
|
||||
["Two by Two", False, [], ['Progressive Weapons']],
|
||||
["Two by Two", False, ['Progressive Tools', 'Progressive Tools'], ['Bucket', 'Progressive Tools']],
|
||||
["Two by Two", False, [], ['Bucket']],
|
||||
["Two by Two", False, ['Progressive Tools', 'Progressive Tools'], ['Bucket', 'Progressive Tools']],
|
||||
["Two by Two", False, ['Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Progressive Tools', 'Progressive Tools', 'Progressive Weapons']],
|
||||
["Two by Two", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Bucket', 'Progressive Weapons']],
|
||||
["Two by Two", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Progressive Tools', 'Progressive Tools', 'Progressive Weapons']],
|
||||
])
|
||||
|
||||
def test_42023(self):
|
||||
|
||||
@@ -19,13 +19,13 @@ class WorldTestBase(unittest.TestCase):
|
||||
if self.auto_construct:
|
||||
self.world_setup()
|
||||
|
||||
def world_setup(self) -> None:
|
||||
def world_setup(self, seed: typing.Optional[int] = None) -> None:
|
||||
if not hasattr(self, "game"):
|
||||
raise NotImplementedError("didn't define game name")
|
||||
self.world = MultiWorld(1)
|
||||
self.world.game[1] = self.game
|
||||
self.world.player_name = {1: "Tester"}
|
||||
self.world.set_seed()
|
||||
self.world.set_seed(seed)
|
||||
args = Namespace()
|
||||
for name, option in AutoWorld.AutoWorldRegister.world_types[self.game].option_definitions.items():
|
||||
setattr(args, name, {
|
||||
|
||||
@@ -10,7 +10,7 @@ class TestGoalVanilla(ZillionTestBase):
|
||||
"floppy_req": 6,
|
||||
}
|
||||
|
||||
def test_floppies(self):
|
||||
def test_floppies(self) -> None:
|
||||
self.collect_by_name(["Apple", "Champ", "Red ID Card"])
|
||||
self.assertBeatable(False) # 0 floppies
|
||||
floppies = self.get_items_by_name("Floppy Disk")
|
||||
@@ -26,19 +26,20 @@ class TestGoalVanilla(ZillionTestBase):
|
||||
self.assertEqual(self.count("Floppy Disk"), 7)
|
||||
self.assertBeatable(True)
|
||||
|
||||
def test_with_everything(self):
|
||||
def test_with_everything(self) -> None:
|
||||
self.collect_by_name(["Apple", "Champ", "Red ID Card", "Floppy Disk"])
|
||||
self.assertBeatable(True)
|
||||
|
||||
def test_no_jump(self):
|
||||
def test_no_jump(self) -> None:
|
||||
self.collect_by_name(["Champ", "Red ID Card", "Floppy Disk"])
|
||||
self.assertBeatable(False)
|
||||
|
||||
def test_no_gun(self):
|
||||
def test_no_gun(self) -> None:
|
||||
self.ensure_gun_3_requirement()
|
||||
self.collect_by_name(["Apple", "Red ID Card", "Floppy Disk"])
|
||||
self.assertBeatable(False)
|
||||
|
||||
def test_no_red(self):
|
||||
def test_no_red(self) -> None:
|
||||
self.collect_by_name(["Apple", "Champ", "Floppy Disk"])
|
||||
self.assertBeatable(False)
|
||||
|
||||
@@ -50,7 +51,7 @@ class TestGoalBalanced(ZillionTestBase):
|
||||
"gun_levels": "balanced",
|
||||
}
|
||||
|
||||
def test_jump(self):
|
||||
def test_jump(self) -> None:
|
||||
self.collect_by_name(["Red ID Card", "Floppy Disk", "Zillion"])
|
||||
self.assertBeatable(False) # not enough jump
|
||||
opas = self.get_items_by_name("Opa-Opa")
|
||||
@@ -60,7 +61,8 @@ class TestGoalBalanced(ZillionTestBase):
|
||||
self.collect(opas[1:])
|
||||
self.assertBeatable(True)
|
||||
|
||||
def test_guns(self):
|
||||
def test_guns(self) -> None:
|
||||
self.ensure_gun_3_requirement()
|
||||
self.collect_by_name(["Red ID Card", "Floppy Disk", "Opa-Opa"])
|
||||
self.assertBeatable(False) # not enough gun
|
||||
guns = self.get_items_by_name("Zillion")
|
||||
@@ -78,7 +80,7 @@ class TestGoalRestrictive(ZillionTestBase):
|
||||
"gun_levels": "restrictive",
|
||||
}
|
||||
|
||||
def test_jump(self):
|
||||
def test_jump(self) -> None:
|
||||
self.collect_by_name(["Champ", "Red ID Card", "Floppy Disk", "Zillion"])
|
||||
self.assertBeatable(False) # not enough jump
|
||||
self.collect_by_name("Opa-Opa")
|
||||
@@ -86,7 +88,8 @@ class TestGoalRestrictive(ZillionTestBase):
|
||||
self.collect_by_name("Apple")
|
||||
self.assertBeatable(True)
|
||||
|
||||
def test_guns(self):
|
||||
def test_guns(self) -> None:
|
||||
self.ensure_gun_3_requirement()
|
||||
self.collect_by_name(["Apple", "Red ID Card", "Floppy Disk", "Opa-Opa"])
|
||||
self.assertBeatable(False) # not enough gun
|
||||
self.collect_by_name("Zillion")
|
||||
@@ -104,15 +107,17 @@ class TestGoalAppleStart(ZillionTestBase):
|
||||
"zillion_count": 5
|
||||
}
|
||||
|
||||
def test_guns_jj_first(self):
|
||||
def test_guns_jj_first(self) -> None:
|
||||
""" with low gun levels, 5 Zillion is enough to get JJ to gun 3 """
|
||||
self.ensure_gun_3_requirement()
|
||||
self.collect_by_name(["JJ", "Red ID Card", "Floppy Disk", "Opa-Opa"])
|
||||
self.assertBeatable(False) # not enough gun
|
||||
self.collect_by_name("Zillion")
|
||||
self.assertBeatable(True)
|
||||
|
||||
def test_guns_zillions_first(self):
|
||||
def test_guns_zillions_first(self) -> None:
|
||||
""" with low gun levels, 5 Zillion is enough to get JJ to gun 3 """
|
||||
self.ensure_gun_3_requirement()
|
||||
self.collect_by_name(["Zillion", "Red ID Card", "Floppy Disk", "Opa-Opa"])
|
||||
self.assertBeatable(False) # not enough gun
|
||||
self.collect_by_name("JJ")
|
||||
@@ -129,14 +134,14 @@ class TestGoalChampStart(ZillionTestBase):
|
||||
"opas_per_level": 1
|
||||
}
|
||||
|
||||
def test_jump_jj_first(self):
|
||||
def test_jump_jj_first(self) -> None:
|
||||
""" with low jump levels, 5 level-ups is enough to get JJ to jump 3 """
|
||||
self.collect_by_name(["JJ", "Red ID Card", "Floppy Disk", "Zillion"])
|
||||
self.assertBeatable(False) # not enough jump
|
||||
self.collect_by_name("Opa-Opa")
|
||||
self.assertBeatable(True)
|
||||
|
||||
def test_jump_opa_first(self):
|
||||
def test_jump_opa_first(self) -> None:
|
||||
""" with low jump levels, 5 level-ups is enough to get JJ to jump 3 """
|
||||
self.collect_by_name(["Opa-Opa", "Red ID Card", "Floppy Disk", "Zillion"])
|
||||
self.assertBeatable(False) # not enough jump
|
||||
|
||||
@@ -1,13 +1,20 @@
|
||||
from typing import cast
|
||||
from test.worlds.test_base import WorldTestBase
|
||||
from worlds.zillion.region import ZillionLocation
|
||||
from worlds.zillion import ZillionWorld
|
||||
|
||||
|
||||
class ZillionTestBase(WorldTestBase):
|
||||
game = "Zillion"
|
||||
|
||||
def world_setup(self) -> None:
|
||||
super().world_setup()
|
||||
# make sure game requires gun 3 for tests
|
||||
for location in self.world.get_locations():
|
||||
if isinstance(location, ZillionLocation) and location.name.startswith("O-7"):
|
||||
location.zz_loc.req.gun = 3
|
||||
def ensure_gun_3_requirement(self) -> None:
|
||||
"""
|
||||
There's a low probability that gun 3 is not required.
|
||||
|
||||
This makes sure that gun 3 is required by making all the canisters
|
||||
in O-7 (including key word canisters) require gun 3.
|
||||
"""
|
||||
zz_world = cast(ZillionWorld, self.world.worlds[1])
|
||||
assert zz_world.zz_system.randomizer
|
||||
for zz_loc_name, zz_loc in zz_world.zz_system.randomizer.locations.items():
|
||||
if zz_loc_name.startswith("r15c6"):
|
||||
zz_loc.req.gun = 3
|
||||
|
||||
0
typings/kivy/__init__.pyi
Normal file
0
typings/kivy/__init__.pyi
Normal file
2
typings/kivy/app.pyi
Normal file
2
typings/kivy/app.pyi
Normal file
@@ -0,0 +1,2 @@
|
||||
class App:
|
||||
async def async_run(self) -> None: ...
|
||||
0
typings/kivy/core/__init__.pyi
Normal file
0
typings/kivy/core/__init__.pyi
Normal file
7
typings/kivy/core/text.pyi
Normal file
7
typings/kivy/core/text.pyi
Normal file
@@ -0,0 +1,7 @@
|
||||
from typing import Tuple
|
||||
from ..graphics import FillType_Shape
|
||||
from ..uix.widget import Widget
|
||||
|
||||
|
||||
class Label(FillType_Shape, Widget):
|
||||
def __init__(self, *, text: str, font_size: int, color: Tuple[float, float, float, float]) -> None: ...
|
||||
40
typings/kivy/graphics.pyi
Normal file
40
typings/kivy/graphics.pyi
Normal file
@@ -0,0 +1,40 @@
|
||||
""" FillType_* is not a real kivy type - just something to fill unknown typing. """
|
||||
|
||||
from typing import Sequence
|
||||
|
||||
FillType_Vec = Sequence[int]
|
||||
|
||||
|
||||
class FillType_Drawable:
|
||||
def __init__(self, *, pos: FillType_Vec = ..., size: FillType_Vec = ...) -> None: ...
|
||||
|
||||
|
||||
class FillType_Texture(FillType_Drawable):
|
||||
pass
|
||||
|
||||
|
||||
class FillType_Shape(FillType_Drawable):
|
||||
texture: FillType_Texture
|
||||
|
||||
def __init__(self,
|
||||
*,
|
||||
texture: FillType_Texture = ...,
|
||||
pos: FillType_Vec = ...,
|
||||
size: FillType_Vec = ...) -> None: ...
|
||||
|
||||
|
||||
class Ellipse(FillType_Shape):
|
||||
pass
|
||||
|
||||
|
||||
class Color:
|
||||
def __init__(self, r: float, g: float, b: float, a: float) -> None: ...
|
||||
|
||||
|
||||
class Rectangle(FillType_Shape):
|
||||
def __init__(self,
|
||||
*,
|
||||
source: str = ...,
|
||||
texture: FillType_Texture = ...,
|
||||
pos: FillType_Vec = ...,
|
||||
size: FillType_Vec = ...) -> None: ...
|
||||
0
typings/kivy/uix/__init__.pyi
Normal file
0
typings/kivy/uix/__init__.pyi
Normal file
8
typings/kivy/uix/layout.pyi
Normal file
8
typings/kivy/uix/layout.pyi
Normal file
@@ -0,0 +1,8 @@
|
||||
from typing import Any
|
||||
from .widget import Widget
|
||||
|
||||
|
||||
class Layout(Widget):
|
||||
def add_widget(self, widget: Widget) -> None: ...
|
||||
|
||||
def do_layout(self, *largs: Any, **kwargs: Any) -> None: ...
|
||||
12
typings/kivy/uix/tabbedpanel.pyi
Normal file
12
typings/kivy/uix/tabbedpanel.pyi
Normal file
@@ -0,0 +1,12 @@
|
||||
from .layout import Layout
|
||||
from .widget import Widget
|
||||
|
||||
|
||||
class TabbedPanel(Layout):
|
||||
pass
|
||||
|
||||
|
||||
class TabbedPanelItem(Widget):
|
||||
content: Widget
|
||||
|
||||
def __init__(self, *, text: str = ...) -> None: ...
|
||||
31
typings/kivy/uix/widget.pyi
Normal file
31
typings/kivy/uix/widget.pyi
Normal file
@@ -0,0 +1,31 @@
|
||||
""" FillType_* is not a real kivy type - just something to fill unknown typing. """
|
||||
|
||||
from typing import Any, Optional, Protocol
|
||||
from ..graphics import FillType_Drawable, FillType_Vec
|
||||
|
||||
|
||||
class FillType_BindCallback(Protocol):
|
||||
def __call__(self, *args: Any) -> None: ...
|
||||
|
||||
|
||||
class FillType_Canvas:
|
||||
def add(self, drawable: FillType_Drawable) -> None: ...
|
||||
|
||||
def clear(self) -> None: ...
|
||||
|
||||
def __enter__(self) -> None: ...
|
||||
|
||||
def __exit__(self, *args: Any) -> None: ...
|
||||
|
||||
|
||||
class Widget:
|
||||
canvas: FillType_Canvas
|
||||
width: int
|
||||
pos: FillType_Vec
|
||||
|
||||
def bind(self,
|
||||
*,
|
||||
pos: Optional[FillType_BindCallback] = ...,
|
||||
size: Optional[FillType_BindCallback] = ...) -> None: ...
|
||||
|
||||
def refresh(self) -> None: ...
|
||||
42
worlds/AutoSNIClient.py
Normal file
42
worlds/AutoSNIClient.py
Normal file
@@ -0,0 +1,42 @@
|
||||
|
||||
from __future__ import annotations
|
||||
import abc
|
||||
from typing import TYPE_CHECKING, ClassVar, Dict, Tuple, Any, Optional
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from SNIClient import SNIContext
|
||||
|
||||
|
||||
class AutoSNIClientRegister(abc.ABCMeta):
|
||||
game_handlers: ClassVar[Dict[str, SNIClient]] = {}
|
||||
|
||||
def __new__(cls, name: str, bases: Tuple[type, ...], dct: Dict[str, Any]) -> AutoSNIClientRegister:
|
||||
# construct class
|
||||
new_class = super().__new__(cls, name, bases, dct)
|
||||
if "game" in dct:
|
||||
AutoSNIClientRegister.game_handlers[dct["game"]] = new_class()
|
||||
return new_class
|
||||
|
||||
@staticmethod
|
||||
async def get_handler(ctx: SNIContext) -> Optional[SNIClient]:
|
||||
for _game, handler in AutoSNIClientRegister.game_handlers.items():
|
||||
if await handler.validate_rom(ctx):
|
||||
return handler
|
||||
return None
|
||||
|
||||
|
||||
class SNIClient(abc.ABC, metaclass=AutoSNIClientRegister):
|
||||
|
||||
@abc.abstractmethod
|
||||
async def validate_rom(self, ctx: SNIContext) -> bool:
|
||||
""" TODO: interface documentation here """
|
||||
...
|
||||
|
||||
@abc.abstractmethod
|
||||
async def game_watcher(self, ctx: SNIContext) -> None:
|
||||
""" TODO: interface documentation here """
|
||||
...
|
||||
|
||||
async def deathlink_kill_player(self, ctx: SNIContext) -> None:
|
||||
""" override this with implementation to kill player """
|
||||
pass
|
||||
693
worlds/alttp/Client.py
Normal file
693
worlds/alttp/Client.py
Normal file
@@ -0,0 +1,693 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import asyncio
|
||||
import shutil
|
||||
import time
|
||||
|
||||
import Utils
|
||||
|
||||
from NetUtils import ClientStatus, color
|
||||
from worlds.AutoSNIClient import SNIClient
|
||||
|
||||
from worlds.alttp import Shops, Regions
|
||||
from .Rom import ROM_PLAYER_LIMIT
|
||||
|
||||
snes_logger = logging.getLogger("SNES")
|
||||
|
||||
GAME_ALTTP = "A Link to the Past"
|
||||
|
||||
# FXPAK Pro protocol memory mapping used by SNI
|
||||
ROM_START = 0x000000
|
||||
WRAM_START = 0xF50000
|
||||
WRAM_SIZE = 0x20000
|
||||
SRAM_START = 0xE00000
|
||||
|
||||
ROMNAME_START = SRAM_START + 0x2000
|
||||
ROMNAME_SIZE = 0x15
|
||||
|
||||
INGAME_MODES = {0x07, 0x09, 0x0b}
|
||||
ENDGAME_MODES = {0x19, 0x1a}
|
||||
DEATH_MODES = {0x12}
|
||||
|
||||
SAVEDATA_START = WRAM_START + 0xF000
|
||||
SAVEDATA_SIZE = 0x500
|
||||
|
||||
RECV_PROGRESS_ADDR = SAVEDATA_START + 0x4D0 # 2 bytes
|
||||
RECV_ITEM_ADDR = SAVEDATA_START + 0x4D2 # 1 byte
|
||||
RECV_ITEM_PLAYER_ADDR = SAVEDATA_START + 0x4D3 # 1 byte
|
||||
ROOMID_ADDR = SAVEDATA_START + 0x4D4 # 2 bytes
|
||||
ROOMDATA_ADDR = SAVEDATA_START + 0x4D6 # 1 byte
|
||||
SCOUT_LOCATION_ADDR = SAVEDATA_START + 0x4D7 # 1 byte
|
||||
SCOUTREPLY_LOCATION_ADDR = SAVEDATA_START + 0x4D8 # 1 byte
|
||||
SCOUTREPLY_ITEM_ADDR = SAVEDATA_START + 0x4D9 # 1 byte
|
||||
SCOUTREPLY_PLAYER_ADDR = SAVEDATA_START + 0x4DA # 1 byte
|
||||
SHOP_ADDR = SAVEDATA_START + 0x302 # 2 bytes
|
||||
SHOP_LEN = (len(Shops.shop_table) * 3) + 5
|
||||
|
||||
DEATH_LINK_ACTIVE_ADDR = ROMNAME_START + 0x15 # 1 byte
|
||||
|
||||
location_shop_ids = set([info[0] for name, info in Shops.shop_table.items()])
|
||||
|
||||
location_table_uw = {"Blind's Hideout - Top": (0x11d, 0x10),
|
||||
"Blind's Hideout - Left": (0x11d, 0x20),
|
||||
"Blind's Hideout - Right": (0x11d, 0x40),
|
||||
"Blind's Hideout - Far Left": (0x11d, 0x80),
|
||||
"Blind's Hideout - Far Right": (0x11d, 0x100),
|
||||
'Secret Passage': (0x55, 0x10),
|
||||
'Waterfall Fairy - Left': (0x114, 0x10),
|
||||
'Waterfall Fairy - Right': (0x114, 0x20),
|
||||
"King's Tomb": (0x113, 0x10),
|
||||
'Floodgate Chest': (0x10b, 0x10),
|
||||
"Link's House": (0x104, 0x10),
|
||||
'Kakariko Tavern': (0x103, 0x10),
|
||||
'Chicken House': (0x108, 0x10),
|
||||
"Aginah's Cave": (0x10a, 0x10),
|
||||
"Sahasrahla's Hut - Left": (0x105, 0x10),
|
||||
"Sahasrahla's Hut - Middle": (0x105, 0x20),
|
||||
"Sahasrahla's Hut - Right": (0x105, 0x40),
|
||||
'Kakariko Well - Top': (0x2f, 0x10),
|
||||
'Kakariko Well - Left': (0x2f, 0x20),
|
||||
'Kakariko Well - Middle': (0x2f, 0x40),
|
||||
'Kakariko Well - Right': (0x2f, 0x80),
|
||||
'Kakariko Well - Bottom': (0x2f, 0x100),
|
||||
'Lost Woods Hideout': (0xe1, 0x200),
|
||||
'Lumberjack Tree': (0xe2, 0x200),
|
||||
'Cave 45': (0x11b, 0x400),
|
||||
'Graveyard Cave': (0x11b, 0x200),
|
||||
'Checkerboard Cave': (0x126, 0x200),
|
||||
'Mini Moldorm Cave - Far Left': (0x123, 0x10),
|
||||
'Mini Moldorm Cave - Left': (0x123, 0x20),
|
||||
'Mini Moldorm Cave - Right': (0x123, 0x40),
|
||||
'Mini Moldorm Cave - Far Right': (0x123, 0x80),
|
||||
'Mini Moldorm Cave - Generous Guy': (0x123, 0x400),
|
||||
'Ice Rod Cave': (0x120, 0x10),
|
||||
'Bonk Rock Cave': (0x124, 0x10),
|
||||
'Desert Palace - Big Chest': (0x73, 0x10),
|
||||
'Desert Palace - Torch': (0x73, 0x400),
|
||||
'Desert Palace - Map Chest': (0x74, 0x10),
|
||||
'Desert Palace - Compass Chest': (0x85, 0x10),
|
||||
'Desert Palace - Big Key Chest': (0x75, 0x10),
|
||||
'Desert Palace - Desert Tiles 1 Pot Key': (0x63, 0x400),
|
||||
'Desert Palace - Beamos Hall Pot Key': (0x53, 0x400),
|
||||
'Desert Palace - Desert Tiles 2 Pot Key': (0x43, 0x400),
|
||||
'Desert Palace - Boss': (0x33, 0x800),
|
||||
'Eastern Palace - Compass Chest': (0xa8, 0x10),
|
||||
'Eastern Palace - Big Chest': (0xa9, 0x10),
|
||||
'Eastern Palace - Dark Square Pot Key': (0xba, 0x400),
|
||||
'Eastern Palace - Dark Eyegore Key Drop': (0x99, 0x400),
|
||||
'Eastern Palace - Cannonball Chest': (0xb9, 0x10),
|
||||
'Eastern Palace - Big Key Chest': (0xb8, 0x10),
|
||||
'Eastern Palace - Map Chest': (0xaa, 0x10),
|
||||
'Eastern Palace - Boss': (0xc8, 0x800),
|
||||
'Hyrule Castle - Boomerang Chest': (0x71, 0x10),
|
||||
'Hyrule Castle - Boomerang Guard Key Drop': (0x71, 0x400),
|
||||
'Hyrule Castle - Map Chest': (0x72, 0x10),
|
||||
'Hyrule Castle - Map Guard Key Drop': (0x72, 0x400),
|
||||
"Hyrule Castle - Zelda's Chest": (0x80, 0x10),
|
||||
'Hyrule Castle - Big Key Drop': (0x80, 0x400),
|
||||
'Sewers - Dark Cross': (0x32, 0x10),
|
||||
'Hyrule Castle - Key Rat Key Drop': (0x21, 0x400),
|
||||
'Sewers - Secret Room - Left': (0x11, 0x10),
|
||||
'Sewers - Secret Room - Middle': (0x11, 0x20),
|
||||
'Sewers - Secret Room - Right': (0x11, 0x40),
|
||||
'Sanctuary': (0x12, 0x10),
|
||||
'Castle Tower - Room 03': (0xe0, 0x10),
|
||||
'Castle Tower - Dark Maze': (0xd0, 0x10),
|
||||
'Castle Tower - Dark Archer Key Drop': (0xc0, 0x400),
|
||||
'Castle Tower - Circle of Pots Key Drop': (0xb0, 0x400),
|
||||
'Spectacle Rock Cave': (0xea, 0x400),
|
||||
'Paradox Cave Lower - Far Left': (0xef, 0x10),
|
||||
'Paradox Cave Lower - Left': (0xef, 0x20),
|
||||
'Paradox Cave Lower - Right': (0xef, 0x40),
|
||||
'Paradox Cave Lower - Far Right': (0xef, 0x80),
|
||||
'Paradox Cave Lower - Middle': (0xef, 0x100),
|
||||
'Paradox Cave Upper - Left': (0xff, 0x10),
|
||||
'Paradox Cave Upper - Right': (0xff, 0x20),
|
||||
'Spiral Cave': (0xfe, 0x10),
|
||||
'Tower of Hera - Basement Cage': (0x87, 0x400),
|
||||
'Tower of Hera - Map Chest': (0x77, 0x10),
|
||||
'Tower of Hera - Big Key Chest': (0x87, 0x10),
|
||||
'Tower of Hera - Compass Chest': (0x27, 0x20),
|
||||
'Tower of Hera - Big Chest': (0x27, 0x10),
|
||||
'Tower of Hera - Boss': (0x7, 0x800),
|
||||
'Hype Cave - Top': (0x11e, 0x10),
|
||||
'Hype Cave - Middle Right': (0x11e, 0x20),
|
||||
'Hype Cave - Middle Left': (0x11e, 0x40),
|
||||
'Hype Cave - Bottom': (0x11e, 0x80),
|
||||
'Hype Cave - Generous Guy': (0x11e, 0x400),
|
||||
'Peg Cave': (0x127, 0x400),
|
||||
'Pyramid Fairy - Left': (0x116, 0x10),
|
||||
'Pyramid Fairy - Right': (0x116, 0x20),
|
||||
'Brewery': (0x106, 0x10),
|
||||
'C-Shaped House': (0x11c, 0x10),
|
||||
'Chest Game': (0x106, 0x400),
|
||||
'Mire Shed - Left': (0x10d, 0x10),
|
||||
'Mire Shed - Right': (0x10d, 0x20),
|
||||
'Superbunny Cave - Top': (0xf8, 0x10),
|
||||
'Superbunny Cave - Bottom': (0xf8, 0x20),
|
||||
'Spike Cave': (0x117, 0x10),
|
||||
'Hookshot Cave - Top Right': (0x3c, 0x10),
|
||||
'Hookshot Cave - Top Left': (0x3c, 0x20),
|
||||
'Hookshot Cave - Bottom Right': (0x3c, 0x80),
|
||||
'Hookshot Cave - Bottom Left': (0x3c, 0x40),
|
||||
'Mimic Cave': (0x10c, 0x10),
|
||||
'Swamp Palace - Entrance': (0x28, 0x10),
|
||||
'Swamp Palace - Map Chest': (0x37, 0x10),
|
||||
'Swamp Palace - Pot Row Pot Key': (0x38, 0x400),
|
||||
'Swamp Palace - Trench 1 Pot Key': (0x37, 0x400),
|
||||
'Swamp Palace - Hookshot Pot Key': (0x36, 0x400),
|
||||
'Swamp Palace - Big Chest': (0x36, 0x10),
|
||||
'Swamp Palace - Compass Chest': (0x46, 0x10),
|
||||
'Swamp Palace - Trench 2 Pot Key': (0x35, 0x400),
|
||||
'Swamp Palace - Big Key Chest': (0x35, 0x10),
|
||||
'Swamp Palace - West Chest': (0x34, 0x10),
|
||||
'Swamp Palace - Flooded Room - Left': (0x76, 0x10),
|
||||
'Swamp Palace - Flooded Room - Right': (0x76, 0x20),
|
||||
'Swamp Palace - Waterfall Room': (0x66, 0x10),
|
||||
'Swamp Palace - Waterway Pot Key': (0x16, 0x400),
|
||||
'Swamp Palace - Boss': (0x6, 0x800),
|
||||
"Thieves' Town - Big Key Chest": (0xdb, 0x20),
|
||||
"Thieves' Town - Map Chest": (0xdb, 0x10),
|
||||
"Thieves' Town - Compass Chest": (0xdc, 0x10),
|
||||
"Thieves' Town - Ambush Chest": (0xcb, 0x10),
|
||||
"Thieves' Town - Hallway Pot Key": (0xbc, 0x400),
|
||||
"Thieves' Town - Spike Switch Pot Key": (0xab, 0x400),
|
||||
"Thieves' Town - Attic": (0x65, 0x10),
|
||||
"Thieves' Town - Big Chest": (0x44, 0x10),
|
||||
"Thieves' Town - Blind's Cell": (0x45, 0x10),
|
||||
"Thieves' Town - Boss": (0xac, 0x800),
|
||||
'Skull Woods - Compass Chest': (0x67, 0x10),
|
||||
'Skull Woods - Map Chest': (0x58, 0x20),
|
||||
'Skull Woods - Big Chest': (0x58, 0x10),
|
||||
'Skull Woods - Pot Prison': (0x57, 0x20),
|
||||
'Skull Woods - Pinball Room': (0x68, 0x10),
|
||||
'Skull Woods - Big Key Chest': (0x57, 0x10),
|
||||
'Skull Woods - West Lobby Pot Key': (0x56, 0x400),
|
||||
'Skull Woods - Bridge Room': (0x59, 0x10),
|
||||
'Skull Woods - Spike Corner Key Drop': (0x39, 0x400),
|
||||
'Skull Woods - Boss': (0x29, 0x800),
|
||||
'Ice Palace - Jelly Key Drop': (0x0e, 0x400),
|
||||
'Ice Palace - Compass Chest': (0x2e, 0x10),
|
||||
'Ice Palace - Conveyor Key Drop': (0x3e, 0x400),
|
||||
'Ice Palace - Freezor Chest': (0x7e, 0x10),
|
||||
'Ice Palace - Big Chest': (0x9e, 0x10),
|
||||
'Ice Palace - Iced T Room': (0xae, 0x10),
|
||||
'Ice Palace - Many Pots Pot Key': (0x9f, 0x400),
|
||||
'Ice Palace - Spike Room': (0x5f, 0x10),
|
||||
'Ice Palace - Big Key Chest': (0x1f, 0x10),
|
||||
'Ice Palace - Hammer Block Key Drop': (0x3f, 0x400),
|
||||
'Ice Palace - Map Chest': (0x3f, 0x10),
|
||||
'Ice Palace - Boss': (0xde, 0x800),
|
||||
'Misery Mire - Big Chest': (0xc3, 0x10),
|
||||
'Misery Mire - Map Chest': (0xc3, 0x20),
|
||||
'Misery Mire - Main Lobby': (0xc2, 0x10),
|
||||
'Misery Mire - Bridge Chest': (0xa2, 0x10),
|
||||
'Misery Mire - Spikes Pot Key': (0xb3, 0x400),
|
||||
'Misery Mire - Spike Chest': (0xb3, 0x10),
|
||||
'Misery Mire - Fishbone Pot Key': (0xa1, 0x400),
|
||||
'Misery Mire - Conveyor Crystal Key Drop': (0xc1, 0x400),
|
||||
'Misery Mire - Compass Chest': (0xc1, 0x10),
|
||||
'Misery Mire - Big Key Chest': (0xd1, 0x10),
|
||||
'Misery Mire - Boss': (0x90, 0x800),
|
||||
'Turtle Rock - Compass Chest': (0xd6, 0x10),
|
||||
'Turtle Rock - Roller Room - Left': (0xb7, 0x10),
|
||||
'Turtle Rock - Roller Room - Right': (0xb7, 0x20),
|
||||
'Turtle Rock - Pokey 1 Key Drop': (0xb6, 0x400),
|
||||
'Turtle Rock - Chain Chomps': (0xb6, 0x10),
|
||||
'Turtle Rock - Pokey 2 Key Drop': (0x13, 0x400),
|
||||
'Turtle Rock - Big Key Chest': (0x14, 0x10),
|
||||
'Turtle Rock - Big Chest': (0x24, 0x10),
|
||||
'Turtle Rock - Crystaroller Room': (0x4, 0x10),
|
||||
'Turtle Rock - Eye Bridge - Bottom Left': (0xd5, 0x80),
|
||||
'Turtle Rock - Eye Bridge - Bottom Right': (0xd5, 0x40),
|
||||
'Turtle Rock - Eye Bridge - Top Left': (0xd5, 0x20),
|
||||
'Turtle Rock - Eye Bridge - Top Right': (0xd5, 0x10),
|
||||
'Turtle Rock - Boss': (0xa4, 0x800),
|
||||
'Palace of Darkness - Shooter Room': (0x9, 0x10),
|
||||
'Palace of Darkness - The Arena - Bridge': (0x2a, 0x20),
|
||||
'Palace of Darkness - Stalfos Basement': (0xa, 0x10),
|
||||
'Palace of Darkness - Big Key Chest': (0x3a, 0x10),
|
||||
'Palace of Darkness - The Arena - Ledge': (0x2a, 0x10),
|
||||
'Palace of Darkness - Map Chest': (0x2b, 0x10),
|
||||
'Palace of Darkness - Compass Chest': (0x1a, 0x20),
|
||||
'Palace of Darkness - Dark Basement - Left': (0x6a, 0x10),
|
||||
'Palace of Darkness - Dark Basement - Right': (0x6a, 0x20),
|
||||
'Palace of Darkness - Dark Maze - Top': (0x19, 0x10),
|
||||
'Palace of Darkness - Dark Maze - Bottom': (0x19, 0x20),
|
||||
'Palace of Darkness - Big Chest': (0x1a, 0x10),
|
||||
'Palace of Darkness - Harmless Hellway': (0x1a, 0x40),
|
||||
'Palace of Darkness - Boss': (0x5a, 0x800),
|
||||
'Ganons Tower - Conveyor Cross Pot Key': (0x8b, 0x400),
|
||||
"Ganons Tower - Bob's Torch": (0x8c, 0x400),
|
||||
'Ganons Tower - Hope Room - Left': (0x8c, 0x20),
|
||||
'Ganons Tower - Hope Room - Right': (0x8c, 0x40),
|
||||
'Ganons Tower - Tile Room': (0x8d, 0x10),
|
||||
'Ganons Tower - Compass Room - Top Left': (0x9d, 0x10),
|
||||
'Ganons Tower - Compass Room - Top Right': (0x9d, 0x20),
|
||||
'Ganons Tower - Compass Room - Bottom Left': (0x9d, 0x40),
|
||||
'Ganons Tower - Compass Room - Bottom Right': (0x9d, 0x80),
|
||||
'Ganons Tower - Conveyor Star Pits Pot Key': (0x7b, 0x400),
|
||||
'Ganons Tower - DMs Room - Top Left': (0x7b, 0x10),
|
||||
'Ganons Tower - DMs Room - Top Right': (0x7b, 0x20),
|
||||
'Ganons Tower - DMs Room - Bottom Left': (0x7b, 0x40),
|
||||
'Ganons Tower - DMs Room - Bottom Right': (0x7b, 0x80),
|
||||
'Ganons Tower - Map Chest': (0x8b, 0x10),
|
||||
'Ganons Tower - Double Switch Pot Key': (0x9b, 0x400),
|
||||
'Ganons Tower - Firesnake Room': (0x7d, 0x10),
|
||||
'Ganons Tower - Randomizer Room - Top Left': (0x7c, 0x10),
|
||||
'Ganons Tower - Randomizer Room - Top Right': (0x7c, 0x20),
|
||||
'Ganons Tower - Randomizer Room - Bottom Left': (0x7c, 0x40),
|
||||
'Ganons Tower - Randomizer Room - Bottom Right': (0x7c, 0x80),
|
||||
"Ganons Tower - Bob's Chest": (0x8c, 0x80),
|
||||
'Ganons Tower - Big Chest': (0x8c, 0x10),
|
||||
'Ganons Tower - Big Key Room - Left': (0x1c, 0x20),
|
||||
'Ganons Tower - Big Key Room - Right': (0x1c, 0x40),
|
||||
'Ganons Tower - Big Key Chest': (0x1c, 0x10),
|
||||
'Ganons Tower - Mini Helmasaur Room - Left': (0x3d, 0x10),
|
||||
'Ganons Tower - Mini Helmasaur Room - Right': (0x3d, 0x20),
|
||||
'Ganons Tower - Mini Helmasaur Key Drop': (0x3d, 0x400),
|
||||
'Ganons Tower - Pre-Moldorm Chest': (0x3d, 0x40),
|
||||
'Ganons Tower - Validation Chest': (0x4d, 0x10)}
|
||||
|
||||
boss_locations = {Regions.lookup_name_to_id[name] for name in {'Eastern Palace - Boss',
|
||||
'Desert Palace - Boss',
|
||||
'Tower of Hera - Boss',
|
||||
'Palace of Darkness - Boss',
|
||||
'Swamp Palace - Boss',
|
||||
'Skull Woods - Boss',
|
||||
"Thieves' Town - Boss",
|
||||
'Ice Palace - Boss',
|
||||
'Misery Mire - Boss',
|
||||
'Turtle Rock - Boss',
|
||||
'Sahasrahla'}}
|
||||
|
||||
location_table_uw_id = {Regions.lookup_name_to_id[name]: data for name, data in location_table_uw.items()}
|
||||
|
||||
location_table_npc = {'Mushroom': 0x1000,
|
||||
'King Zora': 0x2,
|
||||
'Sahasrahla': 0x10,
|
||||
'Blacksmith': 0x400,
|
||||
'Magic Bat': 0x8000,
|
||||
'Sick Kid': 0x4,
|
||||
'Library': 0x80,
|
||||
'Potion Shop': 0x2000,
|
||||
'Old Man': 0x1,
|
||||
'Ether Tablet': 0x100,
|
||||
'Catfish': 0x20,
|
||||
'Stumpy': 0x8,
|
||||
'Bombos Tablet': 0x200}
|
||||
|
||||
location_table_npc_id = {Regions.lookup_name_to_id[name]: data for name, data in location_table_npc.items()}
|
||||
|
||||
location_table_ow = {'Flute Spot': 0x2a,
|
||||
'Sunken Treasure': 0x3b,
|
||||
"Zora's Ledge": 0x81,
|
||||
'Lake Hylia Island': 0x35,
|
||||
'Maze Race': 0x28,
|
||||
'Desert Ledge': 0x30,
|
||||
'Master Sword Pedestal': 0x80,
|
||||
'Spectacle Rock': 0x3,
|
||||
'Pyramid': 0x5b,
|
||||
'Digging Game': 0x68,
|
||||
'Bumper Cave Ledge': 0x4a,
|
||||
'Floating Island': 0x5}
|
||||
|
||||
location_table_ow_id = {Regions.lookup_name_to_id[name]: data for name, data in location_table_ow.items()}
|
||||
|
||||
location_table_misc = {'Bottle Merchant': (0x3c9, 0x2),
|
||||
'Purple Chest': (0x3c9, 0x10),
|
||||
"Link's Uncle": (0x3c6, 0x1),
|
||||
'Hobo': (0x3c9, 0x1)}
|
||||
location_table_misc_id = {Regions.lookup_name_to_id[name]: data for name, data in location_table_misc.items()}
|
||||
|
||||
|
||||
async def track_locations(ctx, roomid, roomdata):
|
||||
from SNIClient import snes_read, snes_buffered_write, snes_flush_writes
|
||||
new_locations = []
|
||||
|
||||
def new_check(location_id):
|
||||
new_locations.append(location_id)
|
||||
ctx.locations_checked.add(location_id)
|
||||
location = ctx.location_names[location_id]
|
||||
snes_logger.info(
|
||||
f'New Check: {location} ({len(ctx.locations_checked)}/{len(ctx.missing_locations) + len(ctx.checked_locations)})')
|
||||
|
||||
try:
|
||||
shop_data = await snes_read(ctx, SHOP_ADDR, SHOP_LEN)
|
||||
shop_data_changed = False
|
||||
shop_data = list(shop_data)
|
||||
for cnt, b in enumerate(shop_data):
|
||||
location = Shops.SHOP_ID_START + cnt
|
||||
if int(b) and location not in ctx.locations_checked:
|
||||
new_check(location)
|
||||
if ctx.allow_collect and location in ctx.checked_locations and location not in ctx.locations_checked \
|
||||
and location in ctx.locations_info and ctx.locations_info[location].player != ctx.slot:
|
||||
if not int(b):
|
||||
shop_data[cnt] += 1
|
||||
shop_data_changed = True
|
||||
if shop_data_changed:
|
||||
snes_buffered_write(ctx, SHOP_ADDR, bytes(shop_data))
|
||||
except Exception as e:
|
||||
snes_logger.info(f"Exception: {e}")
|
||||
|
||||
for location_id, (loc_roomid, loc_mask) in location_table_uw_id.items():
|
||||
try:
|
||||
if location_id not in ctx.locations_checked and loc_roomid == roomid and \
|
||||
(roomdata << 4) & loc_mask != 0:
|
||||
new_check(location_id)
|
||||
except Exception as e:
|
||||
snes_logger.exception(f"Exception: {e}")
|
||||
|
||||
uw_begin = 0x129
|
||||
ow_end = uw_end = 0
|
||||
uw_unchecked = {}
|
||||
uw_checked = {}
|
||||
for location, (roomid, mask) in location_table_uw.items():
|
||||
location_id = Regions.lookup_name_to_id[location]
|
||||
if location_id not in ctx.locations_checked:
|
||||
uw_unchecked[location_id] = (roomid, mask)
|
||||
uw_begin = min(uw_begin, roomid)
|
||||
uw_end = max(uw_end, roomid + 1)
|
||||
if ctx.allow_collect and location_id not in boss_locations and location_id in ctx.checked_locations \
|
||||
and location_id not in ctx.locations_checked and location_id in ctx.locations_info \
|
||||
and ctx.locations_info[location_id].player != ctx.slot:
|
||||
uw_begin = min(uw_begin, roomid)
|
||||
uw_end = max(uw_end, roomid + 1)
|
||||
uw_checked[location_id] = (roomid, mask)
|
||||
|
||||
if uw_begin < uw_end:
|
||||
uw_data = await snes_read(ctx, SAVEDATA_START + (uw_begin * 2), (uw_end - uw_begin) * 2)
|
||||
if uw_data is not None:
|
||||
for location_id, (roomid, mask) in uw_unchecked.items():
|
||||
offset = (roomid - uw_begin) * 2
|
||||
roomdata = uw_data[offset] | (uw_data[offset + 1] << 8)
|
||||
if roomdata & mask != 0:
|
||||
new_check(location_id)
|
||||
if uw_checked:
|
||||
uw_data = list(uw_data)
|
||||
for location_id, (roomid, mask) in uw_checked.items():
|
||||
offset = (roomid - uw_begin) * 2
|
||||
roomdata = uw_data[offset] | (uw_data[offset + 1] << 8)
|
||||
roomdata |= mask
|
||||
uw_data[offset] = roomdata & 0xFF
|
||||
uw_data[offset + 1] = roomdata >> 8
|
||||
snes_buffered_write(ctx, SAVEDATA_START + (uw_begin * 2), bytes(uw_data))
|
||||
|
||||
ow_begin = 0x82
|
||||
ow_unchecked = {}
|
||||
ow_checked = {}
|
||||
for location_id, screenid in location_table_ow_id.items():
|
||||
if location_id not in ctx.locations_checked:
|
||||
ow_unchecked[location_id] = screenid
|
||||
ow_begin = min(ow_begin, screenid)
|
||||
ow_end = max(ow_end, screenid + 1)
|
||||
if ctx.allow_collect and location_id in ctx.checked_locations and location_id in ctx.locations_info \
|
||||
and ctx.locations_info[location_id].player != ctx.slot:
|
||||
ow_checked[location_id] = screenid
|
||||
|
||||
if ow_begin < ow_end:
|
||||
ow_data = await snes_read(ctx, SAVEDATA_START + 0x280 + ow_begin, ow_end - ow_begin)
|
||||
if ow_data is not None:
|
||||
for location_id, screenid in ow_unchecked.items():
|
||||
if ow_data[screenid - ow_begin] & 0x40 != 0:
|
||||
new_check(location_id)
|
||||
if ow_checked:
|
||||
ow_data = list(ow_data)
|
||||
for location_id, screenid in ow_checked.items():
|
||||
ow_data[screenid - ow_begin] |= 0x40
|
||||
snes_buffered_write(ctx, SAVEDATA_START + 0x280 + ow_begin, bytes(ow_data))
|
||||
|
||||
if not ctx.locations_checked.issuperset(location_table_npc_id):
|
||||
npc_data = await snes_read(ctx, SAVEDATA_START + 0x410, 2)
|
||||
if npc_data is not None:
|
||||
npc_value_changed = False
|
||||
npc_value = npc_data[0] | (npc_data[1] << 8)
|
||||
for location_id, mask in location_table_npc_id.items():
|
||||
if npc_value & mask != 0 and location_id not in ctx.locations_checked:
|
||||
new_check(location_id)
|
||||
if ctx.allow_collect and location_id not in boss_locations and location_id in ctx.checked_locations \
|
||||
and location_id not in ctx.locations_checked and location_id in ctx.locations_info \
|
||||
and ctx.locations_info[location_id].player != ctx.slot:
|
||||
npc_value |= mask
|
||||
npc_value_changed = True
|
||||
if npc_value_changed:
|
||||
npc_data = bytes([npc_value & 0xFF, npc_value >> 8])
|
||||
snes_buffered_write(ctx, SAVEDATA_START + 0x410, npc_data)
|
||||
|
||||
if not ctx.locations_checked.issuperset(location_table_misc_id):
|
||||
misc_data = await snes_read(ctx, SAVEDATA_START + 0x3c6, 4)
|
||||
if misc_data is not None:
|
||||
misc_data = list(misc_data)
|
||||
misc_data_changed = False
|
||||
for location_id, (offset, mask) in location_table_misc_id.items():
|
||||
assert (0x3c6 <= offset <= 0x3c9)
|
||||
if misc_data[offset - 0x3c6] & mask != 0 and location_id not in ctx.locations_checked:
|
||||
new_check(location_id)
|
||||
if ctx.allow_collect and location_id in ctx.checked_locations and location_id not in ctx.locations_checked \
|
||||
and location_id in ctx.locations_info and ctx.locations_info[location_id].player != ctx.slot:
|
||||
misc_data_changed = True
|
||||
misc_data[offset - 0x3c6] |= mask
|
||||
if misc_data_changed:
|
||||
snes_buffered_write(ctx, SAVEDATA_START + 0x3c6, bytes(misc_data))
|
||||
|
||||
|
||||
if new_locations:
|
||||
await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": new_locations}])
|
||||
await snes_flush_writes(ctx)
|
||||
|
||||
|
||||
def get_alttp_settings(romfile: str):
|
||||
lastSettings = Utils.get_adjuster_settings(GAME_ALTTP)
|
||||
adjustedromfile = ''
|
||||
if lastSettings:
|
||||
choice = 'no'
|
||||
if not hasattr(lastSettings, 'auto_apply') or 'ask' in lastSettings.auto_apply:
|
||||
|
||||
whitelist = {"music", "menuspeed", "heartbeep", "heartcolor", "ow_palettes", "quickswap",
|
||||
"uw_palettes", "sprite", "sword_palettes", "shield_palettes", "hud_palettes",
|
||||
"reduceflashing", "deathlink", "allowcollect"}
|
||||
printed_options = {name: value for name, value in vars(lastSettings).items() if name in whitelist}
|
||||
if hasattr(lastSettings, "sprite_pool"):
|
||||
sprite_pool = {}
|
||||
for sprite in lastSettings.sprite_pool:
|
||||
if sprite in sprite_pool:
|
||||
sprite_pool[sprite] += 1
|
||||
else:
|
||||
sprite_pool[sprite] = 1
|
||||
if sprite_pool:
|
||||
printed_options["sprite_pool"] = sprite_pool
|
||||
import pprint
|
||||
|
||||
from CommonClient import gui_enabled
|
||||
if gui_enabled:
|
||||
|
||||
try:
|
||||
from tkinter import Tk, PhotoImage, Label, LabelFrame, Frame, Button
|
||||
applyPromptWindow = Tk()
|
||||
except Exception as e:
|
||||
logging.error('Could not load tkinter, which is likely not installed.')
|
||||
return '', False
|
||||
|
||||
applyPromptWindow.resizable(False, False)
|
||||
applyPromptWindow.protocol('WM_DELETE_WINDOW', lambda: onButtonClick())
|
||||
logo = PhotoImage(file=Utils.local_path('data', 'icon.png'))
|
||||
applyPromptWindow.tk.call('wm', 'iconphoto', applyPromptWindow._w, logo)
|
||||
applyPromptWindow.wm_title("Last adjuster settings LttP")
|
||||
|
||||
label = LabelFrame(applyPromptWindow,
|
||||
text='Last used adjuster settings were found. Would you like to apply these?')
|
||||
label.grid(column=0, row=0, padx=5, pady=5, ipadx=5, ipady=5)
|
||||
label.grid_columnconfigure(0, weight=1)
|
||||
label.grid_columnconfigure(1, weight=1)
|
||||
label.grid_columnconfigure(2, weight=1)
|
||||
label.grid_columnconfigure(3, weight=1)
|
||||
|
||||
def onButtonClick(answer: str = 'no'):
|
||||
setattr(onButtonClick, 'choice', answer)
|
||||
applyPromptWindow.destroy()
|
||||
|
||||
framedOptions = Frame(label)
|
||||
framedOptions.grid(column=0, columnspan=4, row=0)
|
||||
framedOptions.grid_columnconfigure(0, weight=1)
|
||||
framedOptions.grid_columnconfigure(1, weight=1)
|
||||
framedOptions.grid_columnconfigure(2, weight=1)
|
||||
curRow = 0
|
||||
curCol = 0
|
||||
for name, value in printed_options.items():
|
||||
Label(framedOptions, text=name + ": " + str(value)).grid(column=curCol, row=curRow, padx=5)
|
||||
if (curCol == 2):
|
||||
curRow += 1
|
||||
curCol = 0
|
||||
else:
|
||||
curCol += 1
|
||||
|
||||
yesButton = Button(label, text='Yes', command=lambda: onButtonClick('yes'), width=10)
|
||||
yesButton.grid(column=0, row=1)
|
||||
noButton = Button(label, text='No', command=lambda: onButtonClick('no'), width=10)
|
||||
noButton.grid(column=1, row=1)
|
||||
alwaysButton = Button(label, text='Always', command=lambda: onButtonClick('always'), width=10)
|
||||
alwaysButton.grid(column=2, row=1)
|
||||
neverButton = Button(label, text='Never', command=lambda: onButtonClick('never'), width=10)
|
||||
neverButton.grid(column=3, row=1)
|
||||
|
||||
Utils.tkinter_center_window(applyPromptWindow)
|
||||
applyPromptWindow.mainloop()
|
||||
choice = getattr(onButtonClick, 'choice')
|
||||
else:
|
||||
choice = input(f"Last used adjuster settings were found. Would you like to apply these? \n"
|
||||
f"{pprint.pformat(printed_options)}\n"
|
||||
f"Enter yes, no, always or never: ")
|
||||
if choice and choice.startswith("y"):
|
||||
choice = 'yes'
|
||||
elif choice and "never" in choice:
|
||||
choice = 'no'
|
||||
lastSettings.auto_apply = 'never'
|
||||
Utils.persistent_store("adjuster", GAME_ALTTP, lastSettings)
|
||||
elif choice and "always" in choice:
|
||||
choice = 'yes'
|
||||
lastSettings.auto_apply = 'always'
|
||||
Utils.persistent_store("adjuster", GAME_ALTTP, lastSettings)
|
||||
else:
|
||||
choice = 'no'
|
||||
elif 'never' in lastSettings.auto_apply:
|
||||
choice = 'no'
|
||||
elif 'always' in lastSettings.auto_apply:
|
||||
choice = 'yes'
|
||||
|
||||
if 'yes' in choice:
|
||||
from worlds.alttp.Rom import get_base_rom_path
|
||||
lastSettings.rom = romfile
|
||||
lastSettings.baserom = get_base_rom_path()
|
||||
lastSettings.world = None
|
||||
|
||||
if hasattr(lastSettings, "sprite_pool"):
|
||||
from LttPAdjuster import AdjusterWorld
|
||||
lastSettings.world = AdjusterWorld(getattr(lastSettings, "sprite_pool"))
|
||||
|
||||
adjusted = True
|
||||
import LttPAdjuster
|
||||
_, adjustedromfile = LttPAdjuster.adjust(lastSettings)
|
||||
|
||||
if hasattr(lastSettings, "world"):
|
||||
delattr(lastSettings, "world")
|
||||
else:
|
||||
adjusted = False
|
||||
if adjusted:
|
||||
try:
|
||||
shutil.move(adjustedromfile, romfile)
|
||||
adjustedromfile = romfile
|
||||
except Exception as e:
|
||||
logging.exception(e)
|
||||
else:
|
||||
adjusted = False
|
||||
return adjustedromfile, adjusted
|
||||
|
||||
|
||||
class ALTTPSNIClient(SNIClient):
|
||||
game = "A Link to the Past"
|
||||
|
||||
async def deathlink_kill_player(self, ctx):
|
||||
from SNIClient import DeathState, snes_read, snes_buffered_write, snes_flush_writes
|
||||
invincible = await snes_read(ctx, WRAM_START + 0x037B, 1)
|
||||
last_health = await snes_read(ctx, WRAM_START + 0xF36D, 1)
|
||||
await asyncio.sleep(0.25)
|
||||
health = await snes_read(ctx, WRAM_START + 0xF36D, 1)
|
||||
if not invincible or not last_health or not health:
|
||||
ctx.death_state = DeathState.dead
|
||||
ctx.last_death_link = time.time()
|
||||
return
|
||||
if not invincible[0] and last_health[0] == health[0]:
|
||||
snes_buffered_write(ctx, WRAM_START + 0xF36D, bytes([0])) # set current health to 0
|
||||
snes_buffered_write(ctx, WRAM_START + 0x0373,
|
||||
bytes([8])) # deal 1 full heart of damage at next opportunity
|
||||
|
||||
await snes_flush_writes(ctx)
|
||||
await asyncio.sleep(1)
|
||||
|
||||
gamemode = await snes_read(ctx, WRAM_START + 0x10, 1)
|
||||
if not gamemode or gamemode[0] in DEATH_MODES:
|
||||
ctx.death_state = DeathState.dead
|
||||
|
||||
|
||||
async def validate_rom(self, ctx):
|
||||
from SNIClient import snes_read, snes_buffered_write, snes_flush_writes
|
||||
|
||||
rom_name = await snes_read(ctx, ROMNAME_START, ROMNAME_SIZE)
|
||||
if rom_name is None or rom_name == bytes([0] * ROMNAME_SIZE) or rom_name[:2] != b"AP":
|
||||
return False
|
||||
|
||||
ctx.game = self.game
|
||||
ctx.items_handling = 0b001 # full local
|
||||
|
||||
ctx.rom = rom_name
|
||||
|
||||
death_link = await snes_read(ctx, DEATH_LINK_ACTIVE_ADDR, 1)
|
||||
|
||||
if death_link:
|
||||
ctx.allow_collect = bool(death_link[0] & 0b100)
|
||||
ctx.death_link_allow_survive = bool(death_link[0] & 0b10)
|
||||
await ctx.update_death_link(bool(death_link[0] & 0b1))
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def game_watcher(self, ctx):
|
||||
from SNIClient import snes_read, snes_buffered_write, snes_flush_writes
|
||||
gamemode = await snes_read(ctx, WRAM_START + 0x10, 1)
|
||||
if "DeathLink" in ctx.tags and gamemode and ctx.last_death_link + 1 < time.time():
|
||||
currently_dead = gamemode[0] in DEATH_MODES
|
||||
await ctx.handle_deathlink_state(currently_dead)
|
||||
|
||||
gameend = await snes_read(ctx, SAVEDATA_START + 0x443, 1)
|
||||
game_timer = await snes_read(ctx, SAVEDATA_START + 0x42E, 4)
|
||||
if gamemode is None or gameend is None or game_timer is None or \
|
||||
(gamemode[0] not in INGAME_MODES and gamemode[0] not in ENDGAME_MODES):
|
||||
return
|
||||
|
||||
if gameend[0]:
|
||||
if not ctx.finished_game:
|
||||
await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}])
|
||||
ctx.finished_game = True
|
||||
|
||||
if gamemode in ENDGAME_MODES: # triforce room and credits
|
||||
return
|
||||
|
||||
data = await snes_read(ctx, RECV_PROGRESS_ADDR, 8)
|
||||
if data is None:
|
||||
return
|
||||
|
||||
recv_index = data[0] | (data[1] << 8)
|
||||
recv_item = data[2]
|
||||
roomid = data[4] | (data[5] << 8)
|
||||
roomdata = data[6]
|
||||
scout_location = data[7]
|
||||
|
||||
if recv_index < len(ctx.items_received) and recv_item == 0:
|
||||
item = ctx.items_received[recv_index]
|
||||
recv_index += 1
|
||||
logging.info('Received %s from %s (%s) (%d/%d in list)' % (
|
||||
color(ctx.item_names[item.item], 'red', 'bold'),
|
||||
color(ctx.player_names[item.player], 'yellow'),
|
||||
ctx.location_names[item.location], recv_index, len(ctx.items_received)))
|
||||
|
||||
snes_buffered_write(ctx, RECV_PROGRESS_ADDR,
|
||||
bytes([recv_index & 0xFF, (recv_index >> 8) & 0xFF]))
|
||||
snes_buffered_write(ctx, RECV_ITEM_ADDR,
|
||||
bytes([item.item]))
|
||||
snes_buffered_write(ctx, RECV_ITEM_PLAYER_ADDR,
|
||||
bytes([min(ROM_PLAYER_LIMIT, item.player) if item.player != ctx.slot else 0]))
|
||||
if scout_location > 0 and scout_location in ctx.locations_info:
|
||||
snes_buffered_write(ctx, SCOUTREPLY_LOCATION_ADDR,
|
||||
bytes([scout_location]))
|
||||
snes_buffered_write(ctx, SCOUTREPLY_ITEM_ADDR,
|
||||
bytes([ctx.locations_info[scout_location].item]))
|
||||
snes_buffered_write(ctx, SCOUTREPLY_PLAYER_ADDR,
|
||||
bytes([min(ROM_PLAYER_LIMIT, ctx.locations_info[scout_location].player)]))
|
||||
|
||||
await snes_flush_writes(ctx)
|
||||
|
||||
if scout_location > 0 and scout_location not in ctx.locations_scouted:
|
||||
ctx.locations_scouted.add(scout_location)
|
||||
await ctx.send_msgs([{"cmd": "LocationScouts", "locations": [scout_location]}])
|
||||
await track_locations(ctx, roomid, roomdata)
|
||||
@@ -15,6 +15,7 @@ from .Items import item_init_table, item_name_groups, item_table, GetBeemizerIte
|
||||
from .Options import alttp_options, smallkey_shuffle
|
||||
from .Regions import lookup_name_to_id, create_regions, mark_light_world_regions, lookup_vanilla_location_to_entrance, \
|
||||
is_main_entrance
|
||||
from .Client import ALTTPSNIClient
|
||||
from .Rom import LocalRom, patch_rom, patch_race_rom, check_enemizer, patch_enemizer, apply_rom_settings, \
|
||||
get_hash_string, get_base_rom_path, LttPDeltaPatch
|
||||
from .Rules import set_rules
|
||||
|
||||
@@ -2,75 +2,69 @@ import logging
|
||||
import asyncio
|
||||
|
||||
from NetUtils import ClientStatus, color
|
||||
from SNIClient import Context, snes_buffered_write, snes_flush_writes, snes_read
|
||||
from Patch import GAME_DKC3
|
||||
from worlds.AutoSNIClient import SNIClient
|
||||
|
||||
snes_logger = logging.getLogger("SNES")
|
||||
|
||||
# DKC3 - DKC3_TODO: Check these values
|
||||
# FXPAK Pro protocol memory mapping used by SNI
|
||||
ROM_START = 0x000000
|
||||
WRAM_START = 0xF50000
|
||||
WRAM_SIZE = 0x20000
|
||||
SRAM_START = 0xE00000
|
||||
|
||||
SAVEDATA_START = WRAM_START + 0xF000
|
||||
SAVEDATA_SIZE = 0x500
|
||||
|
||||
DKC3_ROMNAME_START = 0x00FFC0
|
||||
DKC3_ROMHASH_START = 0x7FC0
|
||||
ROMNAME_SIZE = 0x15
|
||||
ROMHASH_SIZE = 0x15
|
||||
|
||||
DKC3_RECV_PROGRESS_ADDR = WRAM_START + 0x632 # DKC3_TODO: Find a permanent home for this
|
||||
DKC3_RECV_PROGRESS_ADDR = WRAM_START + 0x632
|
||||
DKC3_FILE_NAME_ADDR = WRAM_START + 0x5D9
|
||||
DEATH_LINK_ACTIVE_ADDR = DKC3_ROMNAME_START + 0x15 # DKC3_TODO: Find a permanent home for this
|
||||
|
||||
|
||||
async def deathlink_kill_player(ctx: Context):
|
||||
pass
|
||||
#if ctx.game == GAME_DKC3:
|
||||
class DKC3SNIClient(SNIClient):
|
||||
game = "Donkey Kong Country 3"
|
||||
|
||||
async def deathlink_kill_player(self, ctx):
|
||||
pass
|
||||
# DKC3_TODO: Handle Receiving Deathlink
|
||||
|
||||
|
||||
async def dkc3_rom_init(ctx: Context):
|
||||
if not ctx.rom:
|
||||
ctx.finished_game = False
|
||||
ctx.death_link_allow_survive = False
|
||||
game_name = await snes_read(ctx, DKC3_ROMNAME_START, 0x15)
|
||||
if game_name is None or game_name != b"DONKEY KONG COUNTRY 3":
|
||||
return False
|
||||
else:
|
||||
ctx.game = GAME_DKC3
|
||||
ctx.items_handling = 0b111 # remote items
|
||||
async def validate_rom(self, ctx):
|
||||
from SNIClient import snes_buffered_write, snes_flush_writes, snes_read
|
||||
|
||||
rom = await snes_read(ctx, DKC3_ROMHASH_START, ROMHASH_SIZE)
|
||||
if rom is None or rom == bytes([0] * ROMHASH_SIZE):
|
||||
rom_name = await snes_read(ctx, DKC3_ROMHASH_START, ROMHASH_SIZE)
|
||||
if rom_name is None or rom_name == bytes([0] * ROMHASH_SIZE) or rom_name[:2] != b"D3":
|
||||
return False
|
||||
|
||||
ctx.rom = rom
|
||||
ctx.game = self.game
|
||||
ctx.items_handling = 0b111 # remote items
|
||||
|
||||
ctx.rom = rom_name
|
||||
|
||||
#death_link = await snes_read(ctx, DEATH_LINK_ACTIVE_ADDR, 1)
|
||||
## DKC3_TODO: Handle Deathlink
|
||||
#if death_link:
|
||||
# ctx.allow_collect = bool(death_link[0] & 0b100)
|
||||
# await ctx.update_death_link(bool(death_link[0] & 0b1))
|
||||
return True
|
||||
return True
|
||||
|
||||
|
||||
async def dkc3_game_watcher(ctx: Context):
|
||||
if ctx.game == GAME_DKC3:
|
||||
async def game_watcher(self, ctx):
|
||||
from SNIClient import snes_buffered_write, snes_flush_writes, snes_read
|
||||
# DKC3_TODO: Handle Deathlink
|
||||
save_file_name = await snes_read(ctx, DKC3_FILE_NAME_ADDR, 0x5)
|
||||
if save_file_name is None or save_file_name[0] == 0x00:
|
||||
if save_file_name is None or save_file_name[0] == 0x00 or save_file_name == bytes([0x55] * 0x05):
|
||||
# We haven't loaded a save file
|
||||
return
|
||||
|
||||
new_checks = []
|
||||
from worlds.dkc3.Rom import location_rom_data, item_rom_data, boss_location_ids, level_unlock_map
|
||||
location_ram_data = await snes_read(ctx, WRAM_START + 0x5FE, 0x81)
|
||||
for loc_id, loc_data in location_rom_data.items():
|
||||
if loc_id not in ctx.locations_checked:
|
||||
data = await snes_read(ctx, WRAM_START + loc_data[0], 1)
|
||||
masked_data = data[0] & (1 << loc_data[1])
|
||||
data = location_ram_data[loc_data[0] - 0x5FE]
|
||||
masked_data = data & (1 << loc_data[1])
|
||||
bit_set = (masked_data != 0)
|
||||
invert_bit = ((len(loc_data) >= 3) and loc_data[2])
|
||||
if bit_set != invert_bit:
|
||||
@@ -78,8 +72,9 @@ async def dkc3_game_watcher(ctx: Context):
|
||||
new_checks.append(loc_id)
|
||||
|
||||
verify_save_file_name = await snes_read(ctx, DKC3_FILE_NAME_ADDR, 0x5)
|
||||
if verify_save_file_name is None or verify_save_file_name[0] == 0x00 or verify_save_file_name != save_file_name:
|
||||
if verify_save_file_name is None or verify_save_file_name[0] == 0x00 or verify_save_file_name == bytes([0x55] * 0x05) or verify_save_file_name != save_file_name:
|
||||
# We have somehow exited the save file (or worse)
|
||||
ctx.rom = None
|
||||
return
|
||||
|
||||
rom = await snes_read(ctx, DKC3_ROMHASH_START, ROMHASH_SIZE)
|
||||
@@ -184,8 +179,9 @@ async def dkc3_game_watcher(ctx: Context):
|
||||
|
||||
await snes_flush_writes(ctx)
|
||||
|
||||
# DKC3_TODO: This method of collect should work, however it does not unlock the next level correctly when previous is flagged
|
||||
# Handle Collected Locations
|
||||
levels_to_tiles = await snes_read(ctx, ROM_START + 0x3FF800, 0x60)
|
||||
tiles_to_levels = await snes_read(ctx, ROM_START + 0x3FF860, 0x60)
|
||||
for loc_id in ctx.checked_locations:
|
||||
if loc_id not in ctx.locations_checked and loc_id not in boss_location_ids:
|
||||
loc_data = location_rom_data[loc_id]
|
||||
@@ -193,30 +189,24 @@ async def dkc3_game_watcher(ctx: Context):
|
||||
invert_bit = ((len(loc_data) >= 3) and loc_data[2])
|
||||
if not invert_bit:
|
||||
masked_data = data[0] | (1 << loc_data[1])
|
||||
#print("Collected Location: ", hex(loc_data[0]), " | ", loc_data[1])
|
||||
snes_buffered_write(ctx, WRAM_START + loc_data[0], bytes([masked_data]))
|
||||
|
||||
if (loc_data[1] == 1):
|
||||
# Make the next levels accessible
|
||||
level_id = loc_data[0] - 0x632
|
||||
levels_to_tiles = await snes_read(ctx, ROM_START + 0x3FF800, 0x60)
|
||||
tiles_to_levels = await snes_read(ctx, ROM_START + 0x3FF860, 0x60)
|
||||
tile_id = levels_to_tiles[level_id] if levels_to_tiles[level_id] != 0xFF else level_id
|
||||
tile_id = tile_id + 0x632
|
||||
#print("Tile ID: ", hex(tile_id))
|
||||
if tile_id in level_unlock_map:
|
||||
for next_level_address in level_unlock_map[tile_id]:
|
||||
next_level_id = next_level_address - 0x632
|
||||
next_tile_id = tiles_to_levels[next_level_id] if tiles_to_levels[next_level_id] != 0xFF else next_level_id
|
||||
next_tile_id = next_tile_id + 0x632
|
||||
#print("Next Level ID: ", hex(next_tile_id))
|
||||
next_data = await snes_read(ctx, WRAM_START + next_tile_id, 1)
|
||||
snes_buffered_write(ctx, WRAM_START + next_tile_id, bytes([next_data[0] | 0x01]))
|
||||
|
||||
await snes_flush_writes(ctx)
|
||||
else:
|
||||
masked_data = data[0] & ~(1 << loc_data[1])
|
||||
print("Collected Inverted Location: ", hex(loc_data[0]), " | ", loc_data[1])
|
||||
snes_buffered_write(ctx, WRAM_START + loc_data[0], bytes([masked_data]))
|
||||
await snes_flush_writes(ctx)
|
||||
ctx.locations_checked.add(loc_id)
|
||||
|
||||
@@ -11,6 +11,7 @@ from .Regions import create_regions, connect_regions
|
||||
from .Levels import level_list
|
||||
from .Rules import set_rules
|
||||
from .Names import ItemName, LocationName
|
||||
from .Client import DKC3SNIClient
|
||||
from ..AutoWorld import WebWorld, World
|
||||
from .Rom import LocalRom, patch_rom, get_base_rom_path, DKC3DeltaPatch
|
||||
import Patch
|
||||
|
||||
@@ -157,6 +157,7 @@ function on_player_created(event)
|
||||
{%- if silo == 2 %}
|
||||
check_spawn_silo(game.players[event.player_index].force)
|
||||
{%- endif %}
|
||||
dumpInfo(player.force)
|
||||
end
|
||||
script.on_event(defines.events.on_player_created, on_player_created)
|
||||
|
||||
@@ -491,6 +492,7 @@ commands.add_command("ap-sync", "Used by the Archipelago client to get progress
|
||||
["death_link"] = DEATH_LINK,
|
||||
["energy"] = chain_lookup(forcedata, "energy"),
|
||||
["energy_bridges"] = chain_lookup(forcedata, "energy_bridges"),
|
||||
["multiplayer"] = #game.players > 1,
|
||||
}
|
||||
|
||||
for tech_name, tech in pairs(force.technologies) do
|
||||
@@ -596,5 +598,13 @@ commands.add_command("ap-energylink", "Used by the Archipelago client to manage
|
||||
global.forcedata[force].energy = global.forcedata[force].energy + change
|
||||
end)
|
||||
|
||||
commands.add_command("toggle-ap-send-filter", "Toggle filtering of item sends that get displayed in-game to only those that involve you.", function(call)
|
||||
log("Player command toggle-ap-send-filter") -- notifies client
|
||||
end)
|
||||
|
||||
commands.add_command("toggle-ap-chat", "Toggle sending of chat messages from players on the Factorio server to Archipelago.", function(call)
|
||||
log("Player command toggle-ap-chat") -- notifies client
|
||||
end)
|
||||
|
||||
-- data
|
||||
progressive_technologies = {{ dict_to_lua(progressive_technology_table) }}
|
||||
|
||||
@@ -132,6 +132,8 @@ This allows you to host your own Factorio game.
|
||||
|
||||
For additional client features, issue the `/help` command in the Archipelago Client. Once connected to the AP server,
|
||||
you can also issue the `!help` command to learn about additional commands like `!hint`.
|
||||
For more information about the commands you can use, see the [Commands Guide](/tutorial/Archipelago/commands/en) and
|
||||
[Other Settings](#other-settings).
|
||||
|
||||
## Allowing Other People to Join Your Game
|
||||
|
||||
@@ -141,19 +143,34 @@ you can also issue the `!help` command to learn about additional commands like `
|
||||
4. Provide your IP address to anyone you want to join your game, and have them follow the steps for
|
||||
"Connecting to Someone Else's Factorio Game" above.
|
||||
|
||||
## Other Settings
|
||||
|
||||
- By default, all item sends are displayed in-game. In larger async seeds this may become overly spammy.
|
||||
To hide all item sends that are not to or from your factory, do one of the following:
|
||||
- Type `/toggle-ap-send-filter` in-game
|
||||
- Type `/toggle_send_filter` in the Archipelago Client
|
||||
- In your `host.yaml` set
|
||||
```
|
||||
factorio_options:
|
||||
filter_item_sends: true
|
||||
```
|
||||
- By default, in-game chat is bridged to Archipelago. If you prefer to be able to speak privately, you can disable this
|
||||
feature by doing one of the following:
|
||||
- Type `/toggle-ap-chat` in-game
|
||||
- Type `/toggle_chat` in the Archipelago Client
|
||||
- In your `host.yaml` set
|
||||
```
|
||||
factorio_options:
|
||||
bridge_chat_out: false
|
||||
```
|
||||
Note that this will also disable `!` commands from within the game, and that it will not affect incoming chat.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
In case any problems should occur, the Archipelago Client will create a file `FactorioClient.txt` in the `/logs`. The
|
||||
contents of this file may help you troubleshoot an issue on your own and is vital for requesting help from other people
|
||||
in Archipelago.
|
||||
|
||||
## Commands in game
|
||||
|
||||
Once you have connected to the server successfully using the Archipelago Factorio Client you should see a message
|
||||
stating you can get help using Archipelago commands by typing `!help`. Commands cannot currently be sent from within
|
||||
the Factorio session, but you can send them from the Archipelago Factorio Client. For more information about the commands
|
||||
you can use see the [commands guide](/tutorial/Archipelago/commands/en).
|
||||
|
||||
## Additional Resources
|
||||
|
||||
- Alternate Tutorial by
|
||||
|
||||
@@ -106,7 +106,7 @@ settings. If a game can be rolled it **must** have a settings section even if it
|
||||
|
||||
Some options in Archipelago can be used by every game but must still be placed within the relevant game's section.
|
||||
|
||||
Currently, these options are `start_inventory`, `start_hints`, `local_items`, `non_local_items`, `start_location_hints`
|
||||
Currently, these options are `start_inventory`, `early_items`, `start_hints`, `local_items`, `non_local_items`, `start_location_hints`
|
||||
, `exclude_locations`, and various plando options.
|
||||
|
||||
See the plando guide for more info on plando options. Plando
|
||||
@@ -115,6 +115,8 @@ guide: [Archipelago Plando Guide](/tutorial/Archipelago/plando/en)
|
||||
* `start_inventory` will give any items defined here to you at the beginning of your game. The format for this must be
|
||||
the name as it appears in the game files and the amount you would like to start with. For example `Rupees(5): 6` which
|
||||
will give you 30 rupees.
|
||||
* `early_items` is formatted in the same way as `start_inventory` and will force the number of each item specified to be
|
||||
forced into locations that are reachable from the start, before obtaining any items.
|
||||
* `start_hints` gives you free server hints for the defined item/s at the beginning of the game allowing you to hint for
|
||||
the location without using any hint points.
|
||||
* `local_items` will force any items you want to be in your world instead of being in another world.
|
||||
@@ -172,6 +174,8 @@ A Link to the Past:
|
||||
- Quake
|
||||
non_local_items:
|
||||
- Moon Pearl
|
||||
early_items:
|
||||
Flute: 1
|
||||
start_location_hints:
|
||||
- Spike Cave
|
||||
priority_locations:
|
||||
@@ -235,6 +239,9 @@ Timespinner:
|
||||
* `local_items` forces the `Bombos`, `Ether`, and `Quake` medallions to all be placed within our own world, meaning we
|
||||
have to find it ourselves.
|
||||
* `non_local_items` forces the `Moon Pearl` to be placed in someone else's world, meaning we won't be able to find it.
|
||||
* `early_items` forces the `Flute` to be placed in a location that is available from the beginning of the game ("Sphere
|
||||
1"). Since it is not specified in `local_items` or `non_local_items`, it can be placed one of these locations in any
|
||||
world.
|
||||
* `start_location_hints` gives us a starting hint for the `Spike Cave` location available at the beginning of the
|
||||
multiworld that can be used for no cost.
|
||||
* `priority_locations` forces a progression item to be placed on the `Link's House` location.
|
||||
|
||||
@@ -173,7 +173,7 @@ def set_advancement_rules(world: MultiWorld, player: int):
|
||||
state.can_reach("Hero of the Village", "Location", player)) # Bad Omen, Hero of the Village
|
||||
set_rule(world.get_location("Bullseye", player), lambda state: state.has("Archery", player) and state.has("Progressive Tools", player, 2) and state._mc_has_iron_ingots(player))
|
||||
set_rule(world.get_location("Spooky Scary Skeleton", player), lambda state: state._mc_basic_combat(player))
|
||||
set_rule(world.get_location("Two by Two", player), lambda state: state._mc_has_iron_ingots(player) and state._mc_can_adventure(player)) # shears > seagrass > turtles; nether > striders; gold carrots > horses skips ingots
|
||||
set_rule(world.get_location("Two by Two", player), lambda state: state._mc_has_iron_ingots(player) and state.has("Bucket", player) and state._mc_can_adventure(player)) # shears > seagrass > turtles; buckets of tropical fish > axolotls; nether > striders; gold carrots > horses skips ingots
|
||||
# set_rule(world.get_location("Stone Age", player), lambda state: True)
|
||||
set_rule(world.get_location("Two Birds, One Arrow", player), lambda state: state._mc_craft_crossbow(player) and state._mc_can_enchant(player))
|
||||
# set_rule(world.get_location("We Need to Go Deeper", player), lambda state: True)
|
||||
|
||||
@@ -291,6 +291,11 @@ level_logic = {
|
||||
"Progressive Throw/Catch",
|
||||
],
|
||||
{ # Additive
|
||||
("Sharp Knife", 1.0),
|
||||
("Dish Scrubber", 1.0),
|
||||
("Clean Dishes", 0.5),
|
||||
("Guest Patience", 0.25),
|
||||
("Burn Leniency", 0.25),
|
||||
},
|
||||
)
|
||||
),
|
||||
|
||||
0
worlds/sa2b/Names/__init__.py
Normal file
0
worlds/sa2b/Names/__init__.py
Normal file
@@ -1,5 +1,7 @@
|
||||
from BaseClasses import Item, ItemClassification
|
||||
from BaseClasses import Item, ItemClassification, MultiWorld
|
||||
import typing
|
||||
|
||||
from .Options import get_option_value
|
||||
from .MissionTables import vanilla_mission_req_table
|
||||
|
||||
|
||||
@@ -9,6 +11,7 @@ class ItemData(typing.NamedTuple):
|
||||
number: typing.Optional[int]
|
||||
classification: ItemClassification = ItemClassification.useful
|
||||
quantity: int = 1
|
||||
parent_item: str = None
|
||||
|
||||
|
||||
class StarcraftWoLItem(Item):
|
||||
@@ -48,51 +51,51 @@ item_table = {
|
||||
"Progressive Ship Weapon": ItemData(105 + SC2WOL_ITEM_ID_OFFSET, "Upgrade", 8, quantity=3),
|
||||
"Progressive Ship Armor": ItemData(106 + SC2WOL_ITEM_ID_OFFSET, "Upgrade", 10, quantity=3),
|
||||
|
||||
"Projectile Accelerator (Bunker)": ItemData(200 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 0),
|
||||
"Neosteel Bunker (Bunker)": ItemData(201 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 1),
|
||||
"Titanium Housing (Missile Turret)": ItemData(202 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 2, classification=ItemClassification.filler),
|
||||
"Hellstorm Batteries (Missile Turret)": ItemData(203 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 3),
|
||||
"Advanced Construction (SCV)": ItemData(204 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 4),
|
||||
"Dual-Fusion Welders (SCV)": ItemData(205 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 5),
|
||||
"Projectile Accelerator (Bunker)": ItemData(200 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 0, parent_item="Bunker"),
|
||||
"Neosteel Bunker (Bunker)": ItemData(201 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 1, parent_item="Bunker"),
|
||||
"Titanium Housing (Missile Turret)": ItemData(202 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 2, classification=ItemClassification.filler, parent_item="Missile Turret"),
|
||||
"Hellstorm Batteries (Missile Turret)": ItemData(203 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 3, parent_item="Missile Turret"),
|
||||
"Advanced Construction (SCV)": ItemData(204 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 4, parent_item="SCV"),
|
||||
"Dual-Fusion Welders (SCV)": ItemData(205 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 5, parent_item="SCV"),
|
||||
"Fire-Suppression System (Building)": ItemData(206 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 6, classification=ItemClassification.filler),
|
||||
"Orbital Command (Building)": ItemData(207 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 7),
|
||||
"Stimpack (Marine)": ItemData(208 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 8),
|
||||
"Combat Shield (Marine)": ItemData(209 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 9, classification=ItemClassification.progression),
|
||||
"Advanced Medic Facilities (Medic)": ItemData(210 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 10, classification=ItemClassification.progression),
|
||||
"Stabilizer Medpacks (Medic)": ItemData(211 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 11, classification=ItemClassification.progression),
|
||||
"Incinerator Gauntlets (Firebat)": ItemData(212 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 12, classification=ItemClassification.filler),
|
||||
"Juggernaut Plating (Firebat)": ItemData(213 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 13),
|
||||
"Concussive Shells (Marauder)": ItemData(214 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 14),
|
||||
"Kinetic Foam (Marauder)": ItemData(215 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 15),
|
||||
"U-238 Rounds (Reaper)": ItemData(216 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 16),
|
||||
"G-4 Clusterbomb (Reaper)": ItemData(217 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 17, classification=ItemClassification.progression),
|
||||
"Stimpack (Marine)": ItemData(208 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 8, parent_item="Marine"),
|
||||
"Combat Shield (Marine)": ItemData(209 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 9, classification=ItemClassification.progression, parent_item="Marine"),
|
||||
"Advanced Medic Facilities (Medic)": ItemData(210 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 10, classification=ItemClassification.progression, parent_item="Medic"),
|
||||
"Stabilizer Medpacks (Medic)": ItemData(211 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 11, classification=ItemClassification.progression, parent_item="Medic"),
|
||||
"Incinerator Gauntlets (Firebat)": ItemData(212 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 12, classification=ItemClassification.filler, parent_item="Firebat"),
|
||||
"Juggernaut Plating (Firebat)": ItemData(213 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 13, parent_item="Firebat"),
|
||||
"Concussive Shells (Marauder)": ItemData(214 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 14, parent_item="Marauder"),
|
||||
"Kinetic Foam (Marauder)": ItemData(215 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 15, parent_item="Marauder"),
|
||||
"U-238 Rounds (Reaper)": ItemData(216 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 16, parent_item="Reaper"),
|
||||
"G-4 Clusterbomb (Reaper)": ItemData(217 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 17, classification=ItemClassification.progression, parent_item="Reaper"),
|
||||
|
||||
"Twin-Linked Flamethrower (Hellion)": ItemData(300 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 0, classification=ItemClassification.filler),
|
||||
"Thermite Filaments (Hellion)": ItemData(301 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 1),
|
||||
"Cerberus Mine (Vulture)": ItemData(302 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 2, classification=ItemClassification.filler),
|
||||
"Replenishable Magazine (Vulture)": ItemData(303 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 3, classification=ItemClassification.filler),
|
||||
"Multi-Lock Weapons System (Goliath)": ItemData(304 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 4),
|
||||
"Ares-Class Targeting System (Goliath)": ItemData(305 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 5),
|
||||
"Tri-Lithium Power Cell (Diamondback)": ItemData(306 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 6, classification=ItemClassification.filler),
|
||||
"Shaped Hull (Diamondback)": ItemData(307 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 7, classification=ItemClassification.filler),
|
||||
"Maelstrom Rounds (Siege Tank)": ItemData(308 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 8),
|
||||
"Shaped Blast (Siege Tank)": ItemData(309 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 9),
|
||||
"Rapid Deployment Tube (Medivac)": ItemData(310 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 10, classification=ItemClassification.filler),
|
||||
"Advanced Healing AI (Medivac)": ItemData(311 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 11, classification=ItemClassification.filler),
|
||||
"Tomahawk Power Cells (Wraith)": ItemData(312 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 12, classification=ItemClassification.filler),
|
||||
"Displacement Field (Wraith)": ItemData(313 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 13, classification=ItemClassification.filler),
|
||||
"Ripwave Missiles (Viking)": ItemData(314 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 14),
|
||||
"Phobos-Class Weapons System (Viking)": ItemData(315 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 15),
|
||||
"Cross-Spectrum Dampeners (Banshee)": ItemData(316 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 16, classification=ItemClassification.filler),
|
||||
"Shockwave Missile Battery (Banshee)": ItemData(317 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 17),
|
||||
"Missile Pods (Battlecruiser)": ItemData(318 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 18, classification=ItemClassification.filler),
|
||||
"Defensive Matrix (Battlecruiser)": ItemData(319 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 19, classification=ItemClassification.filler),
|
||||
"Ocular Implants (Ghost)": ItemData(320 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 20),
|
||||
"Crius Suit (Ghost)": ItemData(321 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 21),
|
||||
"Psionic Lash (Spectre)": ItemData(322 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 22, classification=ItemClassification.progression),
|
||||
"Nyx-Class Cloaking Module (Spectre)": ItemData(323 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 23),
|
||||
"330mm Barrage Cannon (Thor)": ItemData(324 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 24, classification=ItemClassification.filler),
|
||||
"Immortality Protocol (Thor)": ItemData(325 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 25, classification=ItemClassification.filler),
|
||||
"Twin-Linked Flamethrower (Hellion)": ItemData(300 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 0, classification=ItemClassification.filler, parent_item="Hellion"),
|
||||
"Thermite Filaments (Hellion)": ItemData(301 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 1, parent_item="Hellion"),
|
||||
"Cerberus Mine (Vulture)": ItemData(302 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 2, classification=ItemClassification.filler, parent_item="Vulture"),
|
||||
"Replenishable Magazine (Vulture)": ItemData(303 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 3, classification=ItemClassification.filler, parent_item="Vulture"),
|
||||
"Multi-Lock Weapons System (Goliath)": ItemData(304 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 4, parent_item="Goliath"),
|
||||
"Ares-Class Targeting System (Goliath)": ItemData(305 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 5, parent_item="Goliath"),
|
||||
"Tri-Lithium Power Cell (Diamondback)": ItemData(306 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 6, classification=ItemClassification.filler, parent_item="Diamondback"),
|
||||
"Shaped Hull (Diamondback)": ItemData(307 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 7, classification=ItemClassification.filler, parent_item="Diamondback"),
|
||||
"Maelstrom Rounds (Siege Tank)": ItemData(308 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 8, classification=ItemClassification.progression, parent_item="Siege Tank"),
|
||||
"Shaped Blast (Siege Tank)": ItemData(309 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 9, parent_item="Siege Tank"),
|
||||
"Rapid Deployment Tube (Medivac)": ItemData(310 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 10, classification=ItemClassification.filler, parent_item="Medivac"),
|
||||
"Advanced Healing AI (Medivac)": ItemData(311 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 11, classification=ItemClassification.filler, parent_item="Medivac"),
|
||||
"Tomahawk Power Cells (Wraith)": ItemData(312 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 12, classification=ItemClassification.filler, parent_item="Wraith"),
|
||||
"Displacement Field (Wraith)": ItemData(313 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 13, classification=ItemClassification.filler, parent_item="Wraith"),
|
||||
"Ripwave Missiles (Viking)": ItemData(314 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 14, parent_item="Viking"),
|
||||
"Phobos-Class Weapons System (Viking)": ItemData(315 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 15, parent_item="Viking"),
|
||||
"Cross-Spectrum Dampeners (Banshee)": ItemData(316 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 16, classification=ItemClassification.filler, parent_item="Banshee"),
|
||||
"Shockwave Missile Battery (Banshee)": ItemData(317 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 17, parent_item="Banshee"),
|
||||
"Missile Pods (Battlecruiser)": ItemData(318 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 18, classification=ItemClassification.filler, parent_item="Battlecruiser"),
|
||||
"Defensive Matrix (Battlecruiser)": ItemData(319 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 19, classification=ItemClassification.filler, parent_item="Battlecruiser"),
|
||||
"Ocular Implants (Ghost)": ItemData(320 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 20, parent_item="Ghost"),
|
||||
"Crius Suit (Ghost)": ItemData(321 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 21, parent_item="Ghost"),
|
||||
"Psionic Lash (Spectre)": ItemData(322 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 22, classification=ItemClassification.progression, parent_item="Spectre"),
|
||||
"Nyx-Class Cloaking Module (Spectre)": ItemData(323 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 23, parent_item="Spectre"),
|
||||
"330mm Barrage Cannon (Thor)": ItemData(324 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 24, classification=ItemClassification.filler, parent_item="Thor"),
|
||||
"Immortality Protocol (Thor)": ItemData(325 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 25, classification=ItemClassification.filler, parent_item="Thor"),
|
||||
|
||||
"Bunker": ItemData(400 + SC2WOL_ITEM_ID_OFFSET, "Building", 0, classification=ItemClassification.progression),
|
||||
"Missile Turret": ItemData(401 + SC2WOL_ITEM_ID_OFFSET, "Building", 1, classification=ItemClassification.progression),
|
||||
@@ -117,16 +120,16 @@ item_table = {
|
||||
"Science Vessel": ItemData(607 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 7, classification=ItemClassification.progression),
|
||||
"Tech Reactor": ItemData(608 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 8),
|
||||
"Orbital Strike": ItemData(609 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 9),
|
||||
"Shrike Turret": ItemData(610 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 10),
|
||||
"Fortified Bunker": ItemData(611 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 11),
|
||||
"Planetary Fortress": ItemData(612 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 12),
|
||||
"Perdition Turret": ItemData(613 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 13),
|
||||
"Shrike Turret": ItemData(610 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 10, parent_item="Bunker"),
|
||||
"Fortified Bunker": ItemData(611 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 11, parent_item="Bunker"),
|
||||
"Planetary Fortress": ItemData(612 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 12, classification=ItemClassification.progression),
|
||||
"Perdition Turret": ItemData(613 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 13, classification=ItemClassification.progression),
|
||||
"Predator": ItemData(614 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 14, classification=ItemClassification.filler),
|
||||
"Hercules": ItemData(615 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 15, classification=ItemClassification.progression),
|
||||
"Cellular Reactor": ItemData(616 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 16, classification=ItemClassification.filler),
|
||||
"Regenerative Bio-Steel": ItemData(617 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 17, classification=ItemClassification.filler),
|
||||
"Hive Mind Emulator": ItemData(618 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 18),
|
||||
"Psi Disrupter": ItemData(619 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 19, classification=ItemClassification.filler),
|
||||
"Hive Mind Emulator": ItemData(618 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 18, ItemClassification.progression),
|
||||
"Psi Disrupter": ItemData(619 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 19, classification=ItemClassification.progression),
|
||||
|
||||
"Zealot": ItemData(700 + SC2WOL_ITEM_ID_OFFSET, "Protoss", 0, classification=ItemClassification.progression),
|
||||
"Stalker": ItemData(701 + SC2WOL_ITEM_ID_OFFSET, "Protoss", 1, classification=ItemClassification.progression),
|
||||
@@ -141,15 +144,33 @@ item_table = {
|
||||
"+15 Starting Minerals": ItemData(800 + SC2WOL_ITEM_ID_OFFSET, "Minerals", 15, quantity=0, classification=ItemClassification.filler),
|
||||
"+15 Starting Vespene": ItemData(801 + SC2WOL_ITEM_ID_OFFSET, "Vespene", 15, quantity=0, classification=ItemClassification.filler),
|
||||
"+2 Starting Supply": ItemData(802 + SC2WOL_ITEM_ID_OFFSET, "Supply", 2, quantity=0, classification=ItemClassification.filler),
|
||||
|
||||
# "Keystone Piece": ItemData(850 + SC2WOL_ITEM_ID_OFFSET, "Goal", 0, quantity=0, classification=ItemClassification.progression_skip_balancing)
|
||||
}
|
||||
|
||||
basic_unit: typing.Tuple[str, ...] = (
|
||||
|
||||
basic_units = {
|
||||
'Marine',
|
||||
'Marauder',
|
||||
'Firebat',
|
||||
'Hellion',
|
||||
'Vulture'
|
||||
)
|
||||
}
|
||||
|
||||
advanced_basic_units = {
|
||||
'Reaper',
|
||||
'Goliath',
|
||||
'Diamondback',
|
||||
'Viking'
|
||||
}
|
||||
|
||||
|
||||
def get_basic_units(world: MultiWorld, player: int) -> typing.Set[str]:
|
||||
if get_option_value(world, player, 'required_tactics') > 0:
|
||||
return basic_units.union(advanced_basic_units)
|
||||
else:
|
||||
return basic_units
|
||||
|
||||
|
||||
item_name_groups = {}
|
||||
for item, data in item_table.items():
|
||||
@@ -161,6 +182,22 @@ filler_items: typing.Tuple[str, ...] = (
|
||||
'+15 Starting Vespene'
|
||||
)
|
||||
|
||||
defense_ratings = {
|
||||
"Siege Tank": 5,
|
||||
"Maelstrom Rounds": 2,
|
||||
"Planetary Fortress": 3,
|
||||
# Bunker w/ Marine/Marauder: 3,
|
||||
"Perdition Turret": 2,
|
||||
"Missile Turret": 2,
|
||||
"Vulture": 2
|
||||
}
|
||||
zerg_defense_ratings = {
|
||||
"Perdition Turret": 2,
|
||||
# Bunker w/ Firebat
|
||||
"Hive Mind Emulator": 3,
|
||||
"Psi Disruptor": 3
|
||||
}
|
||||
|
||||
lookup_id_to_name: typing.Dict[int, str] = {data.code: item_name for item_name, data in get_full_item_list().items() if
|
||||
data.code}
|
||||
# Map type to expected int
|
||||
@@ -176,4 +213,5 @@ type_flaggroups: typing.Dict[str, int] = {
|
||||
"Minerals": 8,
|
||||
"Vespene": 9,
|
||||
"Supply": 10,
|
||||
"Goal": 11
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from typing import List, Tuple, Optional, Callable, NamedTuple
|
||||
from BaseClasses import MultiWorld
|
||||
from .Options import get_option_value
|
||||
|
||||
from BaseClasses import Location
|
||||
|
||||
@@ -19,6 +20,7 @@ class LocationData(NamedTuple):
|
||||
|
||||
def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[LocationData, ...]:
|
||||
# Note: rules which are ended with or True are rules identified as needed later when restricted units is an option
|
||||
logic_level = get_option_value(world, player, 'required_tactics')
|
||||
location_table: List[LocationData] = [
|
||||
LocationData("Liberation Day", "Liberation Day: Victory", SC2WOL_LOC_ID_OFFSET + 100),
|
||||
LocationData("Liberation Day", "Liberation Day: First Statue", SC2WOL_LOC_ID_OFFSET + 101),
|
||||
@@ -32,26 +34,33 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L
|
||||
LocationData("The Outlaws", "The Outlaws: Rebel Base", SC2WOL_LOC_ID_OFFSET + 201,
|
||||
lambda state: state._sc2wol_has_common_unit(world, player)),
|
||||
LocationData("Zero Hour", "Zero Hour: Victory", SC2WOL_LOC_ID_OFFSET + 300,
|
||||
lambda state: state._sc2wol_has_common_unit(world, player)),
|
||||
lambda state: state._sc2wol_has_common_unit(world, player) and
|
||||
state._sc2wol_defense_rating(world, player, True) >= 2 and
|
||||
(logic_level > 0 or state._sc2wol_has_anti_air(world, player))),
|
||||
LocationData("Zero Hour", "Zero Hour: First Group Rescued", SC2WOL_LOC_ID_OFFSET + 301),
|
||||
LocationData("Zero Hour", "Zero Hour: Second Group Rescued", SC2WOL_LOC_ID_OFFSET + 302,
|
||||
lambda state: state._sc2wol_has_common_unit(world, player)),
|
||||
LocationData("Zero Hour", "Zero Hour: Third Group Rescued", SC2WOL_LOC_ID_OFFSET + 303,
|
||||
lambda state: state._sc2wol_has_common_unit(world, player)),
|
||||
lambda state: state._sc2wol_has_common_unit(world, player) and
|
||||
state._sc2wol_defense_rating(world, player, True) >= 2),
|
||||
LocationData("Evacuation", "Evacuation: Victory", SC2WOL_LOC_ID_OFFSET + 400,
|
||||
lambda state: state._sc2wol_has_common_unit(world, player) and
|
||||
state._sc2wol_has_competent_anti_air(world, player)),
|
||||
(logic_level > 0 and state._sc2wol_has_anti_air(world, player)
|
||||
or state._sc2wol_has_competent_anti_air(world, player))),
|
||||
LocationData("Evacuation", "Evacuation: First Chysalis", SC2WOL_LOC_ID_OFFSET + 401),
|
||||
LocationData("Evacuation", "Evacuation: Second Chysalis", SC2WOL_LOC_ID_OFFSET + 402,
|
||||
lambda state: state._sc2wol_has_common_unit(world, player)),
|
||||
LocationData("Evacuation", "Evacuation: Third Chysalis", SC2WOL_LOC_ID_OFFSET + 403,
|
||||
lambda state: state._sc2wol_has_common_unit(world, player)),
|
||||
LocationData("Outbreak", "Outbreak: Victory", SC2WOL_LOC_ID_OFFSET + 500,
|
||||
lambda state: state._sc2wol_has_common_unit(world, player) or state.has("Reaper", player)),
|
||||
lambda state: state._sc2wol_defense_rating(world, player, True, False) >= 4 and
|
||||
(state._sc2wol_has_common_unit(world, player) or state.has("Reaper", player))),
|
||||
LocationData("Outbreak", "Outbreak: Left Infestor", SC2WOL_LOC_ID_OFFSET + 501,
|
||||
lambda state: state._sc2wol_has_common_unit(world, player) or state.has("Reaper", player)),
|
||||
lambda state: state._sc2wol_defense_rating(world, player, True, False) >= 2 and
|
||||
(state._sc2wol_has_common_unit(world, player) or state.has("Reaper", player))),
|
||||
LocationData("Outbreak", "Outbreak: Right Infestor", SC2WOL_LOC_ID_OFFSET + 502,
|
||||
lambda state: state._sc2wol_has_common_unit(world, player) or state.has("Reaper", player)),
|
||||
lambda state: state._sc2wol_defense_rating(world, player, True, False) >= 2 and
|
||||
(state._sc2wol_has_common_unit(world, player) or state.has("Reaper", player))),
|
||||
LocationData("Safe Haven", "Safe Haven: Victory", SC2WOL_LOC_ID_OFFSET + 600,
|
||||
lambda state: state._sc2wol_has_common_unit(world, player) and
|
||||
state._sc2wol_has_competent_anti_air(world, player)),
|
||||
@@ -66,38 +75,48 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L
|
||||
state._sc2wol_has_competent_anti_air(world, player)),
|
||||
LocationData("Haven's Fall", "Haven's Fall: Victory", SC2WOL_LOC_ID_OFFSET + 700,
|
||||
lambda state: state._sc2wol_has_common_unit(world, player) and
|
||||
state._sc2wol_has_competent_anti_air(world, player)),
|
||||
state._sc2wol_has_competent_anti_air(world, player) and
|
||||
state._sc2wol_defense_rating(world, player, True) >= 3),
|
||||
LocationData("Haven's Fall", "Haven's Fall: North Hive", SC2WOL_LOC_ID_OFFSET + 701,
|
||||
lambda state: state._sc2wol_has_common_unit(world, player) and
|
||||
state._sc2wol_has_competent_anti_air(world, player)),
|
||||
state._sc2wol_has_competent_anti_air(world, player) and
|
||||
state._sc2wol_defense_rating(world, player, True) >= 3),
|
||||
LocationData("Haven's Fall", "Haven's Fall: East Hive", SC2WOL_LOC_ID_OFFSET + 702,
|
||||
lambda state: state._sc2wol_has_common_unit(world, player) and
|
||||
state._sc2wol_has_competent_anti_air(world, player)),
|
||||
state._sc2wol_has_competent_anti_air(world, player) and
|
||||
state._sc2wol_defense_rating(world, player, True) >= 3),
|
||||
LocationData("Haven's Fall", "Haven's Fall: South Hive", SC2WOL_LOC_ID_OFFSET + 703,
|
||||
lambda state: state._sc2wol_has_common_unit(world, player) and
|
||||
state._sc2wol_has_competent_anti_air(world, player)),
|
||||
state._sc2wol_has_competent_anti_air(world, player) and
|
||||
state._sc2wol_defense_rating(world, player, True) >= 3),
|
||||
LocationData("Smash and Grab", "Smash and Grab: Victory", SC2WOL_LOC_ID_OFFSET + 800,
|
||||
lambda state: state._sc2wol_has_common_unit(world, player) and
|
||||
state._sc2wol_has_competent_anti_air(world, player)),
|
||||
(logic_level > 0 and state._sc2wol_has_anti_air(world, player)
|
||||
or state._sc2wol_has_competent_anti_air(world, player))),
|
||||
LocationData("Smash and Grab", "Smash and Grab: First Relic", SC2WOL_LOC_ID_OFFSET + 801),
|
||||
LocationData("Smash and Grab", "Smash and Grab: Second Relic", SC2WOL_LOC_ID_OFFSET + 802),
|
||||
LocationData("Smash and Grab", "Smash and Grab: Third Relic", SC2WOL_LOC_ID_OFFSET + 803,
|
||||
lambda state: state._sc2wol_has_common_unit(world, player)),
|
||||
lambda state: state._sc2wol_has_common_unit(world, player) and
|
||||
(logic_level > 0 and state._sc2wol_has_anti_air(world, player)
|
||||
or state._sc2wol_has_competent_anti_air(world, player))),
|
||||
LocationData("Smash and Grab", "Smash and Grab: Fourth Relic", SC2WOL_LOC_ID_OFFSET + 804,
|
||||
lambda state: state._sc2wol_has_common_unit(world, player) and
|
||||
state._sc2wol_has_anti_air(world, player)),
|
||||
(logic_level > 0 and state._sc2wol_has_anti_air(world, player)
|
||||
or state._sc2wol_has_competent_anti_air(world, player))),
|
||||
LocationData("The Dig", "The Dig: Victory", SC2WOL_LOC_ID_OFFSET + 900,
|
||||
lambda state: state._sc2wol_has_common_unit(world, player) and
|
||||
state._sc2wol_has_anti_air(world, player) and
|
||||
state._sc2wol_has_heavy_defense(world, player)),
|
||||
lambda state: state._sc2wol_has_anti_air(world, player) and
|
||||
state._sc2wol_defense_rating(world, player, False) >= 7),
|
||||
LocationData("The Dig", "The Dig: Left Relic", SC2WOL_LOC_ID_OFFSET + 901,
|
||||
lambda state: state._sc2wol_has_common_unit(world, player)),
|
||||
lambda state: state._sc2wol_defense_rating(world, player, False) >= 5),
|
||||
LocationData("The Dig", "The Dig: Right Ground Relic", SC2WOL_LOC_ID_OFFSET + 902,
|
||||
lambda state: state._sc2wol_has_common_unit(world, player)),
|
||||
lambda state: state._sc2wol_defense_rating(world, player, False) >= 5),
|
||||
LocationData("The Dig", "The Dig: Right Cliff Relic", SC2WOL_LOC_ID_OFFSET + 903,
|
||||
lambda state: state._sc2wol_has_common_unit(world, player)),
|
||||
lambda state: state._sc2wol_defense_rating(world, player, False) >= 5),
|
||||
LocationData("The Moebius Factor", "The Moebius Factor: Victory", SC2WOL_LOC_ID_OFFSET + 1000,
|
||||
lambda state: state._sc2wol_has_air(world, player) and state._sc2wol_has_anti_air(world, player)),
|
||||
lambda state: state._sc2wol_has_anti_air(world, player) and
|
||||
(state._sc2wol_has_air(world, player)
|
||||
or state.has_any({'Medivac', 'Hercules'}, player)
|
||||
and state._sc2wol_has_common_unit(world, player))),
|
||||
LocationData("The Moebius Factor", "The Moebius Factor: South Rescue", SC2WOL_LOC_ID_OFFSET + 1003,
|
||||
lambda state: state._sc2wol_able_to_rescue(world, player)),
|
||||
LocationData("The Moebius Factor", "The Moebius Factor: Wall Rescue", SC2WOL_LOC_ID_OFFSET + 1004,
|
||||
@@ -109,7 +128,10 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L
|
||||
LocationData("The Moebius Factor", "The Moebius Factor: Alive Inside Rescue", SC2WOL_LOC_ID_OFFSET + 1007,
|
||||
lambda state: state._sc2wol_able_to_rescue(world, player)),
|
||||
LocationData("The Moebius Factor", "The Moebius Factor: Brutalisk", SC2WOL_LOC_ID_OFFSET + 1008,
|
||||
lambda state: state._sc2wol_has_air(world, player)),
|
||||
lambda state: state._sc2wol_has_anti_air(world, player) and
|
||||
(state._sc2wol_has_air(world, player)
|
||||
or state.has_any({'Medivac', 'Hercules'}, player)
|
||||
and state._sc2wol_has_common_unit(world, player))),
|
||||
LocationData("Supernova", "Supernova: Victory", SC2WOL_LOC_ID_OFFSET + 1100,
|
||||
lambda state: state._sc2wol_beats_protoss_deathball(world, player)),
|
||||
LocationData("Supernova", "Supernova: West Relic", SC2WOL_LOC_ID_OFFSET + 1101),
|
||||
@@ -119,37 +141,23 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L
|
||||
LocationData("Supernova", "Supernova: East Relic", SC2WOL_LOC_ID_OFFSET + 1104,
|
||||
lambda state: state._sc2wol_beats_protoss_deathball(world, player)),
|
||||
LocationData("Maw of the Void", "Maw of the Void: Victory", SC2WOL_LOC_ID_OFFSET + 1200,
|
||||
lambda state: state.has('Battlecruiser', player) or
|
||||
state._sc2wol_has_air(world, player) and
|
||||
state._sc2wol_has_competent_anti_air(world, player) and
|
||||
state.has('Science Vessel', player)),
|
||||
lambda state: state._sc2wol_survives_rip_field(world, player)),
|
||||
LocationData("Maw of the Void", "Maw of the Void: Landing Zone Cleared", SC2WOL_LOC_ID_OFFSET + 1201),
|
||||
LocationData("Maw of the Void", "Maw of the Void: Expansion Prisoners", SC2WOL_LOC_ID_OFFSET + 1202,
|
||||
lambda state: state.has('Battlecruiser', player) or
|
||||
state._sc2wol_has_air(world, player) and
|
||||
state._sc2wol_has_competent_anti_air(world, player) and
|
||||
state.has('Science Vessel', player)),
|
||||
lambda state: logic_level > 0 or state._sc2wol_survives_rip_field(world, player)),
|
||||
LocationData("Maw of the Void", "Maw of the Void: South Close Prisoners", SC2WOL_LOC_ID_OFFSET + 1203,
|
||||
lambda state: state.has('Battlecruiser', player) or
|
||||
state._sc2wol_has_air(world, player) and
|
||||
state._sc2wol_has_competent_anti_air(world, player) and
|
||||
state.has('Science Vessel', player)),
|
||||
lambda state: logic_level > 0 or state._sc2wol_survives_rip_field(world, player)),
|
||||
LocationData("Maw of the Void", "Maw of the Void: South Far Prisoners", SC2WOL_LOC_ID_OFFSET + 1204,
|
||||
lambda state: state.has('Battlecruiser', player) or
|
||||
state._sc2wol_has_air(world, player) and
|
||||
state._sc2wol_has_competent_anti_air(world, player) and
|
||||
state.has('Science Vessel', player)),
|
||||
lambda state: state._sc2wol_survives_rip_field(world, player)),
|
||||
LocationData("Maw of the Void", "Maw of the Void: North Prisoners", SC2WOL_LOC_ID_OFFSET + 1205,
|
||||
lambda state: state.has('Battlecruiser', player) or
|
||||
state._sc2wol_has_air(world, player) and
|
||||
state._sc2wol_has_competent_anti_air(world, player) and
|
||||
state.has('Science Vessel', player)),
|
||||
lambda state: state._sc2wol_survives_rip_field(world, player)),
|
||||
LocationData("Devil's Playground", "Devil's Playground: Victory", SC2WOL_LOC_ID_OFFSET + 1300,
|
||||
lambda state: state._sc2wol_has_anti_air(world, player) and (
|
||||
state._sc2wol_has_common_unit(world, player) or state.has("Reaper", player))),
|
||||
lambda state: logic_level > 0 or
|
||||
state._sc2wol_has_anti_air(world, player) and (
|
||||
state._sc2wol_has_common_unit(world, player) or state.has("Reaper", player))),
|
||||
LocationData("Devil's Playground", "Devil's Playground: Tosh's Miners", SC2WOL_LOC_ID_OFFSET + 1301),
|
||||
LocationData("Devil's Playground", "Devil's Playground: Brutalisk", SC2WOL_LOC_ID_OFFSET + 1302,
|
||||
lambda state: state._sc2wol_has_common_unit(world, player) or state.has("Reaper", player)),
|
||||
lambda state: logic_level > 0 or state._sc2wol_has_common_unit(world, player) or state.has("Reaper", player)),
|
||||
LocationData("Welcome to the Jungle", "Welcome to the Jungle: Victory", SC2WOL_LOC_ID_OFFSET + 1400,
|
||||
lambda state: state._sc2wol_has_common_unit(world, player) and
|
||||
state._sc2wol_has_competent_anti_air(world, player)),
|
||||
@@ -176,7 +184,8 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L
|
||||
LocationData("The Great Train Robbery", "The Great Train Robbery: Mid Defiler", SC2WOL_LOC_ID_OFFSET + 1702),
|
||||
LocationData("The Great Train Robbery", "The Great Train Robbery: South Defiler", SC2WOL_LOC_ID_OFFSET + 1703),
|
||||
LocationData("Cutthroat", "Cutthroat: Victory", SC2WOL_LOC_ID_OFFSET + 1800,
|
||||
lambda state: state._sc2wol_has_common_unit(world, player)),
|
||||
lambda state: state._sc2wol_has_common_unit(world, player) and
|
||||
(logic_level > 0 or state._sc2wol_has_anti_air)),
|
||||
LocationData("Cutthroat", "Cutthroat: Mira Han", SC2WOL_LOC_ID_OFFSET + 1801,
|
||||
lambda state: state._sc2wol_has_common_unit(world, player)),
|
||||
LocationData("Cutthroat", "Cutthroat: North Relic", SC2WOL_LOC_ID_OFFSET + 1802,
|
||||
@@ -208,40 +217,44 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L
|
||||
lambda state: state._sc2wol_has_competent_comp(world, player)),
|
||||
LocationData("Media Blitz", "Media Blitz: Science Facility", SC2WOL_LOC_ID_OFFSET + 2004),
|
||||
LocationData("Piercing the Shroud", "Piercing the Shroud: Victory", SC2WOL_LOC_ID_OFFSET + 2100,
|
||||
lambda state: state.has_any({'Combat Shield (Marine)', 'Stabilizer Medpacks (Medic)'}, player)),
|
||||
lambda state: state._sc2wol_has_mm_upgrade(world, player)),
|
||||
LocationData("Piercing the Shroud", "Piercing the Shroud: Holding Cell Relic", SC2WOL_LOC_ID_OFFSET + 2101),
|
||||
LocationData("Piercing the Shroud", "Piercing the Shroud: Brutalisk Relic", SC2WOL_LOC_ID_OFFSET + 2102,
|
||||
lambda state: state.has_any({'Combat Shield (Marine)', 'Stabilizer Medpacks (Medic)'}, player)),
|
||||
lambda state: state._sc2wol_has_mm_upgrade(world, player)),
|
||||
LocationData("Piercing the Shroud", "Piercing the Shroud: First Escape Relic", SC2WOL_LOC_ID_OFFSET + 2103,
|
||||
lambda state: state.has_any({'Combat Shield (Marine)', 'Stabilizer Medpacks (Medic)'}, player)),
|
||||
lambda state: state._sc2wol_has_mm_upgrade(world, player)),
|
||||
LocationData("Piercing the Shroud", "Piercing the Shroud: Second Escape Relic", SC2WOL_LOC_ID_OFFSET + 2104,
|
||||
lambda state: state.has_any({'Combat Shield (Marine)', 'Stabilizer Medpacks (Medic)'}, player)),
|
||||
lambda state: state._sc2wol_has_mm_upgrade(world, player)),
|
||||
LocationData("Piercing the Shroud", "Piercing the Shroud: Brutalisk ", SC2WOL_LOC_ID_OFFSET + 2105,
|
||||
lambda state: state.has_any({'Combat Shield (Marine)', 'Stabilizer Medpacks (Medic)'}, player)),
|
||||
lambda state: state._sc2wol_has_mm_upgrade(world, player)),
|
||||
LocationData("Whispers of Doom", "Whispers of Doom: Victory", SC2WOL_LOC_ID_OFFSET + 2200),
|
||||
LocationData("Whispers of Doom", "Whispers of Doom: First Hatchery", SC2WOL_LOC_ID_OFFSET + 2201),
|
||||
LocationData("Whispers of Doom", "Whispers of Doom: Second Hatchery", SC2WOL_LOC_ID_OFFSET + 2202),
|
||||
LocationData("Whispers of Doom", "Whispers of Doom: Third Hatchery", SC2WOL_LOC_ID_OFFSET + 2203),
|
||||
LocationData("A Sinister Turn", "A Sinister Turn: Victory", SC2WOL_LOC_ID_OFFSET + 2300,
|
||||
lambda state: state._sc2wol_has_protoss_medium_units(world, player)),
|
||||
LocationData("A Sinister Turn", "A Sinister Turn: Robotics Facility", SC2WOL_LOC_ID_OFFSET + 2301),
|
||||
LocationData("A Sinister Turn", "A Sinister Turn: Dark Shrine", SC2WOL_LOC_ID_OFFSET + 2302),
|
||||
LocationData("A Sinister Turn", "A Sinister Turn: Robotics Facility", SC2WOL_LOC_ID_OFFSET + 2301,
|
||||
lambda state: logic_level > 0 or state._sc2wol_has_protoss_common_units(world, player)),
|
||||
LocationData("A Sinister Turn", "A Sinister Turn: Dark Shrine", SC2WOL_LOC_ID_OFFSET + 2302,
|
||||
lambda state: logic_level > 0 or state._sc2wol_has_protoss_common_units(world, player)),
|
||||
LocationData("A Sinister Turn", "A Sinister Turn: Templar Archives", SC2WOL_LOC_ID_OFFSET + 2303,
|
||||
lambda state: state._sc2wol_has_protoss_common_units(world, player)),
|
||||
LocationData("Echoes of the Future", "Echoes of the Future: Victory", SC2WOL_LOC_ID_OFFSET + 2400,
|
||||
lambda state: state._sc2wol_has_protoss_medium_units(world, player)),
|
||||
lambda state: logic_level > 0 or state._sc2wol_has_protoss_medium_units(world, player)),
|
||||
LocationData("Echoes of the Future", "Echoes of the Future: Close Obelisk", SC2WOL_LOC_ID_OFFSET + 2401),
|
||||
LocationData("Echoes of the Future", "Echoes of the Future: West Obelisk", SC2WOL_LOC_ID_OFFSET + 2402,
|
||||
lambda state: state._sc2wol_has_protoss_common_units(world, player)),
|
||||
lambda state: logic_level > 0 or state._sc2wol_has_protoss_common_units(world, player)),
|
||||
LocationData("In Utter Darkness", "In Utter Darkness: Defeat", SC2WOL_LOC_ID_OFFSET + 2500),
|
||||
LocationData("In Utter Darkness", "In Utter Darkness: Protoss Archive", SC2WOL_LOC_ID_OFFSET + 2501,
|
||||
lambda state: state._sc2wol_has_protoss_medium_units(world, player)),
|
||||
LocationData("In Utter Darkness", "In Utter Darkness: Kills", SC2WOL_LOC_ID_OFFSET + 2502,
|
||||
lambda state: state._sc2wol_has_protoss_common_units(world, player)),
|
||||
LocationData("Gates of Hell", "Gates of Hell: Victory", SC2WOL_LOC_ID_OFFSET + 2600,
|
||||
lambda state: state._sc2wol_has_competent_comp(world, player)),
|
||||
lambda state: state._sc2wol_has_competent_comp(world, player) and
|
||||
state._sc2wol_defense_rating(world, player, True) > 6),
|
||||
LocationData("Gates of Hell", "Gates of Hell: Large Army", SC2WOL_LOC_ID_OFFSET + 2601,
|
||||
lambda state: state._sc2wol_has_competent_comp(world, player)),
|
||||
lambda state: state._sc2wol_has_competent_comp(world, player) and
|
||||
state._sc2wol_defense_rating(world, player, True) > 6),
|
||||
LocationData("Belly of the Beast", "Belly of the Beast: Victory", SC2WOL_LOC_ID_OFFSET + 2700),
|
||||
LocationData("Belly of the Beast", "Belly of the Beast: First Charge", SC2WOL_LOC_ID_OFFSET + 2701),
|
||||
LocationData("Belly of the Beast", "Belly of the Beast: Second Charge", SC2WOL_LOC_ID_OFFSET + 2702),
|
||||
@@ -258,15 +271,19 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L
|
||||
lambda state: state._sc2wol_has_competent_comp(world, player)),
|
||||
LocationData("Shatter the Sky", "Shatter the Sky: Leviathan", SC2WOL_LOC_ID_OFFSET + 2805,
|
||||
lambda state: state._sc2wol_has_competent_comp(world, player)),
|
||||
LocationData("All-In", "All-In: Victory", None)
|
||||
LocationData("All-In", "All-In: Victory", None,
|
||||
lambda state: state._sc2wol_final_mission_requirements(world, player))
|
||||
]
|
||||
|
||||
beat_events = []
|
||||
|
||||
for location_data in location_table:
|
||||
for i, location_data in enumerate(location_table):
|
||||
# Removing all item-based logic on No Logic
|
||||
if logic_level == 2:
|
||||
location_table[i] = location_data._replace(rule=Location.access_rule)
|
||||
# Generating Beat event locations
|
||||
if location_data.name.endswith((": Victory", ": Defeat")):
|
||||
beat_events.append(
|
||||
location_data._replace(name="Beat " + location_data.name.rsplit(": ", 1)[0], code=None)
|
||||
)
|
||||
|
||||
return tuple(location_table + beat_events)
|
||||
|
||||
@@ -1,31 +1,43 @@
|
||||
from BaseClasses import MultiWorld
|
||||
from worlds.AutoWorld import LogicMixin
|
||||
from .Options import get_option_value
|
||||
from .Items import get_basic_units, defense_ratings, zerg_defense_ratings
|
||||
|
||||
|
||||
class SC2WoLLogic(LogicMixin):
|
||||
def _sc2wol_has_common_unit(self, world: MultiWorld, player: int) -> bool:
|
||||
return self.has_any({'Marine', 'Marauder', 'Firebat', 'Hellion', 'Vulture'}, player)
|
||||
|
||||
def _sc2wol_has_bunker_unit(self, world: MultiWorld, player: int) -> bool:
|
||||
return self.has_any({'Marine', 'Marauder'}, player)
|
||||
return self.has_any(get_basic_units(world, player), player)
|
||||
|
||||
def _sc2wol_has_air(self, world: MultiWorld, player: int) -> bool:
|
||||
return self.has_any({'Viking', 'Wraith', 'Banshee'}, player) or \
|
||||
self.has_any({'Hercules', 'Medivac'}, player) and self._sc2wol_has_common_unit(world, player)
|
||||
return self.has_any({'Viking', 'Wraith', 'Banshee'}, player) or get_option_value(world, player, 'required_tactics') > 0 \
|
||||
and self.has_any({'Hercules', 'Medivac'}, player) and self._sc2wol_has_common_unit(world, player)
|
||||
|
||||
def _sc2wol_has_air_anti_air(self, world: MultiWorld, player: int) -> bool:
|
||||
return self.has_any({'Viking', 'Wraith'}, player)
|
||||
return self.has('Viking', player) \
|
||||
or get_option_value(world, player, 'required_tactics') > 0 and self.has('Wraith', player)
|
||||
|
||||
def _sc2wol_has_competent_anti_air(self, world: MultiWorld, player: int) -> bool:
|
||||
return self.has_any({'Marine', 'Goliath'}, player) or self._sc2wol_has_air_anti_air(world, player)
|
||||
|
||||
def _sc2wol_has_anti_air(self, world: MultiWorld, player: int) -> bool:
|
||||
return self.has_any({'Missile Turret', 'Thor', 'War Pigs', 'Spartan Company', "Hel's Angel", 'Battlecruiser'}, player) or self._sc2wol_has_competent_anti_air(world, player)
|
||||
return self.has_any({'Missile Turret', 'Thor', 'War Pigs', 'Spartan Company', "Hel's Angel", 'Battlecruiser', 'Wraith'}, player) \
|
||||
or self._sc2wol_has_competent_anti_air(world, player) \
|
||||
or get_option_value(world, player, 'required_tactics') > 0 and self.has_any({'Ghost', 'Spectre'}, player)
|
||||
|
||||
def _sc2wol_has_heavy_defense(self, world: MultiWorld, player: int) -> bool:
|
||||
return (self.has_any({'Siege Tank', 'Vulture'}, player) or
|
||||
self.has('Bunker', player) and self._sc2wol_has_bunker_unit(world, player)) and \
|
||||
self._sc2wol_has_anti_air(world, player)
|
||||
def _sc2wol_defense_rating(self, world: MultiWorld, player: int, zerg_enemy: bool, air_enemy: bool = True) -> bool:
|
||||
defense_score = sum((defense_ratings[item] for item in defense_ratings if self.has(item, player)))
|
||||
if self.has_any({'Marine', 'Marauder'}, player) and self.has('Bunker', player):
|
||||
defense_score += 3
|
||||
if zerg_enemy:
|
||||
defense_score += sum((zerg_defense_ratings[item] for item in zerg_defense_ratings if self.has(item, player)))
|
||||
if self.has('Firebat', player) and self.has('Bunker', player):
|
||||
defense_score += 2
|
||||
if not air_enemy and self.has('Missile Turret', player):
|
||||
defense_score -= defense_ratings['Missile Turret']
|
||||
# Advanced Tactics bumps defense rating requirements down by 2
|
||||
if get_option_value(world, player, 'required_tactics') > 0:
|
||||
defense_score += 2
|
||||
return defense_score
|
||||
|
||||
def _sc2wol_has_competent_comp(self, world: MultiWorld, player: int) -> bool:
|
||||
return (self.has('Marine', player) or self.has('Marauder', player) and
|
||||
@@ -35,25 +47,50 @@ class SC2WoLLogic(LogicMixin):
|
||||
self.has('Siege Tank', player) and self._sc2wol_has_competent_anti_air(world, player)
|
||||
|
||||
def _sc2wol_has_train_killers(self, world: MultiWorld, player: int) -> bool:
|
||||
return (self.has_any({'Siege Tank', 'Diamondback'}, player) or
|
||||
self.has_all({'Reaper', "G-4 Clusterbomb"}, player) or self.has_all({'Spectre', 'Psionic Lash'}, player)
|
||||
or self.has('Marauders', player))
|
||||
return (self.has_any({'Siege Tank', 'Diamondback', 'Marauder'}, player) or get_option_value(world, player, 'required_tactics') > 0
|
||||
and self.has_all({'Reaper', "G-4 Clusterbomb"}, player) or self.has_all({'Spectre', 'Psionic Lash'}, player))
|
||||
|
||||
def _sc2wol_able_to_rescue(self, world: MultiWorld, player: int) -> bool:
|
||||
return self.has_any({'Medivac', 'Hercules', 'Raven', 'Viking'}, player)
|
||||
return self.has_any({'Medivac', 'Hercules', 'Raven', 'Viking'}, player) or get_option_value(world, player, 'required_tactics') > 0
|
||||
|
||||
def _sc2wol_has_protoss_common_units(self, world: MultiWorld, player: int) -> bool:
|
||||
return self.has_any({'Zealot', 'Immortal', 'Stalker', 'Dark Templar'}, player)
|
||||
return self.has_any({'Zealot', 'Immortal', 'Stalker', 'Dark Templar'}, player) \
|
||||
or get_option_value(world, player, 'required_tactics') > 0 and self.has_any({'High Templar', 'Dark Templar'}, player)
|
||||
|
||||
def _sc2wol_has_protoss_medium_units(self, world: MultiWorld, player: int) -> bool:
|
||||
return self._sc2wol_has_protoss_common_units(world, player) and \
|
||||
self.has_any({'Stalker', 'Void Ray', 'Phoenix', 'Carrier'}, player)
|
||||
self.has_any({'Stalker', 'Void Ray', 'Phoenix', 'Carrier'}, player) \
|
||||
or get_option_value(world, player, 'required_tactics') > 0 and self.has_any({'High Templar', 'Dark Templar'}, player)
|
||||
|
||||
def _sc2wol_beats_protoss_deathball(self, world: MultiWorld, player: int) -> bool:
|
||||
return self.has_any({'Banshee', 'Battlecruiser'}, player) and self._sc2wol_has_competent_anti_air or \
|
||||
self._sc2wol_has_competent_comp(world, player) and self._sc2wol_has_air_anti_air(world, player)
|
||||
|
||||
def _sc2wol_has_mm_upgrade(self, world: MultiWorld, player: int) -> bool:
|
||||
return self.has_any({"Combat Shield (Marine)", "Stabilizer Medpacks (Medic)"}, player)
|
||||
|
||||
def _sc2wol_survives_rip_field(self, world: MultiWorld, player: int) -> bool:
|
||||
return self.has("Battlecruiser", player) or \
|
||||
self._sc2wol_has_air(world, player) and \
|
||||
self._sc2wol_has_competent_anti_air(world, player) and \
|
||||
self.has("Science Vessel", player)
|
||||
|
||||
def _sc2wol_has_nukes(self, world: MultiWorld, player: int) -> bool:
|
||||
return get_option_value(world, player, 'required_tactics') > 0 and self.has_any({'Ghost', 'Spectre'}, player)
|
||||
|
||||
def _sc2wol_final_mission_requirements(self, world: MultiWorld, player: int):
|
||||
defense_rating = self._sc2wol_defense_rating(world, player, True)
|
||||
beats_kerrigan = self.has_any({'Marine', 'Banshee', 'Ghost'}, player) or get_option_value(world, player, 'required_tactics') > 0
|
||||
if get_option_value(world, player, 'all_in_map') == 0:
|
||||
# Ground
|
||||
if self.has_any({'Battlecruiser', 'Banshee'}, player):
|
||||
defense_rating += 3
|
||||
return defense_rating >= 12 and beats_kerrigan
|
||||
else:
|
||||
# Air
|
||||
return defense_rating >= 8 and beats_kerrigan \
|
||||
and self.has_any({'Viking', 'Battlecruiser'}, player) \
|
||||
and self.has_any({'Hive Mind Emulator', 'Psi Disruptor', 'Missile Turret'}, player)
|
||||
|
||||
def _sc2wol_cleared_missions(self, world: MultiWorld, player: int, mission_count: int) -> bool:
|
||||
return self.has_group("Missions", player, mission_count)
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
from typing import NamedTuple, Dict, List
|
||||
from typing import NamedTuple, Dict, List, Set
|
||||
|
||||
from BaseClasses import MultiWorld
|
||||
from .Options import get_option_value
|
||||
|
||||
no_build_regions_list = ["Liberation Day", "Breakout", "Ghost of a Chance", "Piercing the Shroud", "Whispers of Doom",
|
||||
"Belly of the Beast"]
|
||||
@@ -12,7 +15,6 @@ hard_regions_list = ["Maw of the Void", "Engine of Destruction", "In Utter Darkn
|
||||
|
||||
class MissionInfo(NamedTuple):
|
||||
id: int
|
||||
extra_locations: int
|
||||
required_world: List[int]
|
||||
category: str
|
||||
number: int = 0 # number of worlds need beaten
|
||||
@@ -62,38 +64,156 @@ vanilla_shuffle_order = [
|
||||
FillMission("all_in", [26, 27], "Char", completion_critical=True, or_requirements=True)
|
||||
]
|
||||
|
||||
mini_campaign_order = [
|
||||
FillMission("no_build", [-1], "Mar Sara", completion_critical=True),
|
||||
FillMission("easy", [0], "Colonist"),
|
||||
FillMission("medium", [1], "Colonist"),
|
||||
FillMission("medium", [0], "Artifact", completion_critical=True),
|
||||
FillMission("medium", [3], "Artifact", number=4, completion_critical=True),
|
||||
FillMission("hard", [4], "Artifact", number=8, completion_critical=True),
|
||||
FillMission("medium", [0], "Covert", number=2),
|
||||
FillMission("hard", [6], "Covert"),
|
||||
FillMission("medium", [0], "Rebellion", number=3),
|
||||
FillMission("hard", [8], "Rebellion"),
|
||||
FillMission("medium", [4], "Prophecy"),
|
||||
FillMission("hard", [10], "Prophecy"),
|
||||
FillMission("hard", [5], "Char", completion_critical=True),
|
||||
FillMission("hard", [5], "Char", completion_critical=True),
|
||||
FillMission("all_in", [12, 13], "Char", completion_critical=True, or_requirements=True)
|
||||
]
|
||||
|
||||
gauntlet_order = [
|
||||
FillMission("no_build", [-1], "I", completion_critical=True),
|
||||
FillMission("easy", [0], "II", completion_critical=True),
|
||||
FillMission("medium", [1], "III", completion_critical=True),
|
||||
FillMission("medium", [2], "IV", completion_critical=True),
|
||||
FillMission("hard", [3], "V", completion_critical=True),
|
||||
FillMission("hard", [4], "VI", completion_critical=True),
|
||||
FillMission("all_in", [5], "Final", completion_critical=True)
|
||||
]
|
||||
|
||||
grid_order = [
|
||||
FillMission("no_build", [-1], "_1"),
|
||||
FillMission("medium", [0], "_1"),
|
||||
FillMission("medium", [1, 6, 3], "_1", or_requirements=True),
|
||||
FillMission("hard", [2, 7], "_1", or_requirements=True),
|
||||
FillMission("easy", [0], "_2"),
|
||||
FillMission("medium", [1, 4], "_2", or_requirements=True),
|
||||
FillMission("hard", [2, 5, 10, 7], "_2", or_requirements=True),
|
||||
FillMission("hard", [3, 6, 11], "_2", or_requirements=True),
|
||||
FillMission("medium", [4, 9, 12], "_3", or_requirements=True),
|
||||
FillMission("hard", [5, 8, 10, 13], "_3", or_requirements=True),
|
||||
FillMission("hard", [6, 9, 11, 14], "_3", or_requirements=True),
|
||||
FillMission("hard", [7, 10], "_3", or_requirements=True),
|
||||
FillMission("hard", [8, 13], "_4", or_requirements=True),
|
||||
FillMission("hard", [9, 12, 14], "_4", or_requirements=True),
|
||||
FillMission("hard", [10, 13], "_4", or_requirements=True),
|
||||
FillMission("all_in", [11, 14], "_4", or_requirements=True)
|
||||
]
|
||||
|
||||
mini_grid_order = [
|
||||
FillMission("no_build", [-1], "_1"),
|
||||
FillMission("medium", [0], "_1"),
|
||||
FillMission("medium", [1, 5], "_1", or_requirements=True),
|
||||
FillMission("easy", [0], "_2"),
|
||||
FillMission("medium", [1, 3], "_2", or_requirements=True),
|
||||
FillMission("hard", [2, 4], "_2", or_requirements=True),
|
||||
FillMission("medium", [3, 7], "_3", or_requirements=True),
|
||||
FillMission("hard", [4, 6], "_3", or_requirements=True),
|
||||
FillMission("all_in", [5, 7], "_3", or_requirements=True)
|
||||
]
|
||||
|
||||
blitz_order = [
|
||||
FillMission("no_build", [-1], "I"),
|
||||
FillMission("easy", [-1], "I"),
|
||||
FillMission("medium", [0, 1], "II", number=1, or_requirements=True),
|
||||
FillMission("medium", [0, 1], "II", number=1, or_requirements=True),
|
||||
FillMission("medium", [0, 1], "III", number=2, or_requirements=True),
|
||||
FillMission("medium", [0, 1], "III", number=2, or_requirements=True),
|
||||
FillMission("hard", [0, 1], "IV", number=3, or_requirements=True),
|
||||
FillMission("hard", [0, 1], "IV", number=3, or_requirements=True),
|
||||
FillMission("hard", [0, 1], "V", number=4, or_requirements=True),
|
||||
FillMission("hard", [0, 1], "V", number=4, or_requirements=True),
|
||||
FillMission("hard", [0, 1], "Final", number=5, or_requirements=True),
|
||||
FillMission("all_in", [0, 1], "Final", number=5, or_requirements=True)
|
||||
]
|
||||
|
||||
mission_orders = [vanilla_shuffle_order, vanilla_shuffle_order, mini_campaign_order, grid_order, mini_grid_order, blitz_order, gauntlet_order]
|
||||
|
||||
|
||||
vanilla_mission_req_table = {
|
||||
"Liberation Day": MissionInfo(1, 7, [], "Mar Sara", completion_critical=True),
|
||||
"The Outlaws": MissionInfo(2, 2, [1], "Mar Sara", completion_critical=True),
|
||||
"Zero Hour": MissionInfo(3, 4, [2], "Mar Sara", completion_critical=True),
|
||||
"Evacuation": MissionInfo(4, 4, [3], "Colonist"),
|
||||
"Outbreak": MissionInfo(5, 3, [4], "Colonist"),
|
||||
"Safe Haven": MissionInfo(6, 4, [5], "Colonist", number=7),
|
||||
"Haven's Fall": MissionInfo(7, 4, [5], "Colonist", number=7),
|
||||
"Smash and Grab": MissionInfo(8, 5, [3], "Artifact", completion_critical=True),
|
||||
"The Dig": MissionInfo(9, 4, [8], "Artifact", number=8, completion_critical=True),
|
||||
"The Moebius Factor": MissionInfo(10, 9, [9], "Artifact", number=11, completion_critical=True),
|
||||
"Supernova": MissionInfo(11, 5, [10], "Artifact", number=14, completion_critical=True),
|
||||
"Maw of the Void": MissionInfo(12, 6, [11], "Artifact", completion_critical=True),
|
||||
"Devil's Playground": MissionInfo(13, 3, [3], "Covert", number=4),
|
||||
"Welcome to the Jungle": MissionInfo(14, 4, [13], "Covert"),
|
||||
"Breakout": MissionInfo(15, 3, [14], "Covert", number=8),
|
||||
"Ghost of a Chance": MissionInfo(16, 6, [14], "Covert", number=8),
|
||||
"The Great Train Robbery": MissionInfo(17, 4, [3], "Rebellion", number=6),
|
||||
"Cutthroat": MissionInfo(18, 5, [17], "Rebellion"),
|
||||
"Engine of Destruction": MissionInfo(19, 6, [18], "Rebellion"),
|
||||
"Media Blitz": MissionInfo(20, 5, [19], "Rebellion"),
|
||||
"Piercing the Shroud": MissionInfo(21, 6, [20], "Rebellion"),
|
||||
"Whispers of Doom": MissionInfo(22, 4, [9], "Prophecy"),
|
||||
"A Sinister Turn": MissionInfo(23, 4, [22], "Prophecy"),
|
||||
"Echoes of the Future": MissionInfo(24, 3, [23], "Prophecy"),
|
||||
"In Utter Darkness": MissionInfo(25, 3, [24], "Prophecy"),
|
||||
"Gates of Hell": MissionInfo(26, 2, [12], "Char", completion_critical=True),
|
||||
"Belly of the Beast": MissionInfo(27, 4, [26], "Char", completion_critical=True),
|
||||
"Shatter the Sky": MissionInfo(28, 5, [26], "Char", completion_critical=True),
|
||||
"All-In": MissionInfo(29, -1, [27, 28], "Char", completion_critical=True, or_requirements=True)
|
||||
"Liberation Day": MissionInfo(1, [], "Mar Sara", completion_critical=True),
|
||||
"The Outlaws": MissionInfo(2, [1], "Mar Sara", completion_critical=True),
|
||||
"Zero Hour": MissionInfo(3, [2], "Mar Sara", completion_critical=True),
|
||||
"Evacuation": MissionInfo(4, [3], "Colonist"),
|
||||
"Outbreak": MissionInfo(5, [4], "Colonist"),
|
||||
"Safe Haven": MissionInfo(6, [5], "Colonist", number=7),
|
||||
"Haven's Fall": MissionInfo(7, [5], "Colonist", number=7),
|
||||
"Smash and Grab": MissionInfo(8, [3], "Artifact", completion_critical=True),
|
||||
"The Dig": MissionInfo(9, [8], "Artifact", number=8, completion_critical=True),
|
||||
"The Moebius Factor": MissionInfo(10, [9], "Artifact", number=11, completion_critical=True),
|
||||
"Supernova": MissionInfo(11, [10], "Artifact", number=14, completion_critical=True),
|
||||
"Maw of the Void": MissionInfo(12, [11], "Artifact", completion_critical=True),
|
||||
"Devil's Playground": MissionInfo(13, [3], "Covert", number=4),
|
||||
"Welcome to the Jungle": MissionInfo(14, [13], "Covert"),
|
||||
"Breakout": MissionInfo(15, [14], "Covert", number=8),
|
||||
"Ghost of a Chance": MissionInfo(16, [14], "Covert", number=8),
|
||||
"The Great Train Robbery": MissionInfo(17, [3], "Rebellion", number=6),
|
||||
"Cutthroat": MissionInfo(18, [17], "Rebellion"),
|
||||
"Engine of Destruction": MissionInfo(19, [18], "Rebellion"),
|
||||
"Media Blitz": MissionInfo(20, [19], "Rebellion"),
|
||||
"Piercing the Shroud": MissionInfo(21, [20], "Rebellion"),
|
||||
"Whispers of Doom": MissionInfo(22, [9], "Prophecy"),
|
||||
"A Sinister Turn": MissionInfo(23, [22], "Prophecy"),
|
||||
"Echoes of the Future": MissionInfo(24, [23], "Prophecy"),
|
||||
"In Utter Darkness": MissionInfo(25, [24], "Prophecy"),
|
||||
"Gates of Hell": MissionInfo(26, [12], "Char", completion_critical=True),
|
||||
"Belly of the Beast": MissionInfo(27, [26], "Char", completion_critical=True),
|
||||
"Shatter the Sky": MissionInfo(28, [26], "Char", completion_critical=True),
|
||||
"All-In": MissionInfo(29, [27, 28], "Char", completion_critical=True, or_requirements=True)
|
||||
}
|
||||
|
||||
lookup_id_to_mission: Dict[int, str] = {
|
||||
data.id: mission_name for mission_name, data in vanilla_mission_req_table.items() if data.id}
|
||||
|
||||
no_build_starting_mission_locations = {
|
||||
"Liberation Day": "Liberation Day: Victory",
|
||||
"Breakout": "Breakout: Victory",
|
||||
"Ghost of a Chance": "Ghost of a Chance: Victory",
|
||||
"Piercing the Shroud": "Piercing the Shroud: Victory",
|
||||
"Whispers of Doom": "Whispers of Doom: Victory",
|
||||
"Belly of the Beast": "Belly of the Beast: Victory",
|
||||
}
|
||||
|
||||
build_starting_mission_locations = {
|
||||
"Zero Hour": "Zero Hour: First Group Rescued",
|
||||
"Evacuation": "Evacuation: First Chysalis",
|
||||
"Devil's Playground": "Devil's Playground: Tosh's Miners"
|
||||
}
|
||||
|
||||
advanced_starting_mission_locations = {
|
||||
"Smash and Grab": "Smash and Grab: First Relic",
|
||||
"The Great Train Robbery": "The Great Train Robbery: North Defiler"
|
||||
}
|
||||
|
||||
|
||||
def get_starting_mission_locations(world: MultiWorld, player: int) -> Set[str]:
|
||||
if get_option_value(world, player, 'shuffle_no_build') or get_option_value(world, player, 'mission_order') < 2:
|
||||
# Always start with a no-build mission unless explicitly relegating them
|
||||
# Vanilla and Vanilla Shuffled always start with a no-build even when relegated
|
||||
return no_build_starting_mission_locations
|
||||
elif get_option_value(world, player, 'required_tactics') > 0:
|
||||
# Advanced Tactics/No Logic add more starting missions to the pool
|
||||
return {**build_starting_mission_locations, **advanced_starting_mission_locations}
|
||||
else:
|
||||
# Standard starting missions when relegate is on
|
||||
return build_starting_mission_locations
|
||||
|
||||
|
||||
alt_final_mission_locations = {
|
||||
"Maw of the Void": "Maw of the Void: Victory",
|
||||
"Engine of Destruction": "Engine of Destruction: Victory",
|
||||
"Supernova": "Supernova: Victory",
|
||||
"Gates of Hell": "Gates of Hell: Victory",
|
||||
"Shatter the Sky": "Shatter the Sky: Victory"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
from typing import Dict
|
||||
from BaseClasses import MultiWorld
|
||||
from Options import Choice, Option, DefaultOnToggle
|
||||
from Options import Choice, Option, Toggle, DefaultOnToggle, ItemSet, OptionSet, Range
|
||||
|
||||
|
||||
class GameDifficulty(Choice):
|
||||
@@ -36,25 +36,75 @@ class AllInMap(Choice):
|
||||
|
||||
|
||||
class MissionOrder(Choice):
|
||||
"""Determines the order the missions are played in.
|
||||
Vanilla: Keeps the standard mission order and branching from the WoL Campaign.
|
||||
Vanilla Shuffled: Keeps same branching paths from the WoL Campaign but randomizes the order of missions within."""
|
||||
"""Determines the order the missions are played in. The last three mission orders end in a random mission.
|
||||
Vanilla (29): Keeps the standard mission order and branching from the WoL Campaign.
|
||||
Vanilla Shuffled (29): Keeps same branching paths from the WoL Campaign but randomizes the order of missions within.
|
||||
Mini Campaign (15): Shorter version of the campaign with randomized missions and optional branches.
|
||||
Grid (16): A 4x4 grid of random missions. Start at the top-left and forge a path towards All-In.
|
||||
Mini Grid (9): A 3x3 version of Grid. Complete the bottom-right mission to win.
|
||||
Blitz (12): 12 random missions that open up very quickly. Complete the bottom-right mission to win.
|
||||
Gauntlet (7): Linear series of 7 random missions to complete the campaign."""
|
||||
display_name = "Mission Order"
|
||||
option_vanilla = 0
|
||||
option_vanilla_shuffled = 1
|
||||
option_mini_campaign = 2
|
||||
option_grid = 3
|
||||
option_mini_grid = 4
|
||||
option_blitz = 5
|
||||
option_gauntlet = 6
|
||||
|
||||
|
||||
class ShuffleProtoss(DefaultOnToggle):
|
||||
"""Determines if the 3 protoss missions are included in the shuffle if Vanilla Shuffled is enabled. If this is
|
||||
not the 3 protoss missions will stay in their vanilla order in the mission order making them optional to complete
|
||||
the game."""
|
||||
"""Determines if the 3 protoss missions are included in the shuffle if Vanilla mission order is not enabled.
|
||||
If turned off with Vanilla Shuffled, the 3 protoss missions will be in their normal position on the Prophecy chain if not shuffled.
|
||||
If turned off with reduced mission settings, the 3 protoss missions will not appear and Protoss units are removed from the pool."""
|
||||
display_name = "Shuffle Protoss Missions"
|
||||
|
||||
|
||||
class RelegateNoBuildMissions(DefaultOnToggle):
|
||||
"""If enabled, all no build missions besides the needed first one will be placed at the end of optional routes so
|
||||
that none of them become required to complete the game. Only takes effect if mission order is not set to vanilla."""
|
||||
display_name = "Relegate No-Build Missions"
|
||||
class ShuffleNoBuild(DefaultOnToggle):
|
||||
"""Determines if the 5 no-build missions are included in the shuffle if Vanilla mission order is not enabled.
|
||||
If turned off with Vanilla Shuffled, one no-build mission will be placed as the first mission and the rest will be placed at the end of optional routes.
|
||||
If turned off with reduced mission settings, the 5 no-build missions will not appear."""
|
||||
display_name = "Shuffle No-Build Missions"
|
||||
|
||||
|
||||
class EarlyUnit(DefaultOnToggle):
|
||||
"""Guarantees that the first mission will contain a unit."""
|
||||
display_name = "Early Unit"
|
||||
|
||||
|
||||
class RequiredTactics(Choice):
|
||||
"""Determines the maximum tactical difficulty of the seed (separate from mission difficulty). Higher settings increase randomness.
|
||||
Standard: All missions can be completed with good micro and macro.
|
||||
Advanced: Completing missions may require relying on starting units and micro-heavy units.
|
||||
No Logic: Units and upgrades may be placed anywhere. LIKELY TO RENDER THE RUN IMPOSSIBLE ON HARDER DIFFICULTIES!"""
|
||||
display_name = "Required Tactics"
|
||||
option_standard = 0
|
||||
option_advanced = 1
|
||||
option_no_logic = 2
|
||||
|
||||
|
||||
class UnitsAlwaysHaveUpgrades(DefaultOnToggle):
|
||||
"""If turned on, both upgrades will be present for each unit and structure in the seed.
|
||||
This usually results in fewer units."""
|
||||
display_name = "Units Always Have Upgrades"
|
||||
|
||||
|
||||
class LockedItems(ItemSet):
|
||||
"""Guarantees that these items will be unlockable"""
|
||||
display_name = "Locked Items"
|
||||
|
||||
|
||||
class ExcludedItems(ItemSet):
|
||||
"""Guarantees that these items will not be unlockable"""
|
||||
display_name = "Excluded Items"
|
||||
|
||||
|
||||
class ExcludedMissions(OptionSet):
|
||||
"""Guarantees that these missions will not appear in the campaign
|
||||
Only applies on shortened mission orders.
|
||||
It may be impossible to build a valid campaign if too many missions are excluded."""
|
||||
display_name = "Excluded Missions"
|
||||
|
||||
|
||||
# noinspection PyTypeChecker
|
||||
@@ -65,14 +115,29 @@ sc2wol_options: Dict[str, Option] = {
|
||||
"all_in_map": AllInMap,
|
||||
"mission_order": MissionOrder,
|
||||
"shuffle_protoss": ShuffleProtoss,
|
||||
"relegate_no_build": RelegateNoBuildMissions
|
||||
"shuffle_no_build": ShuffleNoBuild,
|
||||
"early_unit": EarlyUnit,
|
||||
"required_tactics": RequiredTactics,
|
||||
"units_always_have_upgrades": UnitsAlwaysHaveUpgrades,
|
||||
"locked_items": LockedItems,
|
||||
"excluded_items": ExcludedItems,
|
||||
"excluded_missions": ExcludedMissions
|
||||
}
|
||||
|
||||
|
||||
def get_option_value(world: MultiWorld, player: int, name: str) -> int:
|
||||
option = getattr(world, name, None)
|
||||
|
||||
if option == None:
|
||||
if option is None:
|
||||
return 0
|
||||
|
||||
return int(option[player].value)
|
||||
|
||||
|
||||
def get_option_set_value(world: MultiWorld, player: int, name: str) -> set:
|
||||
option = getattr(world, name, None)
|
||||
|
||||
if option is None:
|
||||
return set()
|
||||
|
||||
return option[player].value
|
||||
|
||||
257
worlds/sc2wol/PoolFilter.py
Normal file
257
worlds/sc2wol/PoolFilter.py
Normal file
@@ -0,0 +1,257 @@
|
||||
from typing import Callable, Dict, List, Set
|
||||
from BaseClasses import MultiWorld, ItemClassification, Item, Location
|
||||
from .Items import item_table
|
||||
from .MissionTables import no_build_regions_list, easy_regions_list, medium_regions_list, hard_regions_list,\
|
||||
mission_orders, get_starting_mission_locations, MissionInfo, vanilla_mission_req_table, alt_final_mission_locations
|
||||
from .Options import get_option_value, get_option_set_value
|
||||
from .LogicMixin import SC2WoLLogic
|
||||
|
||||
# Items with associated upgrades
|
||||
UPGRADABLE_ITEMS = [
|
||||
"Marine", "Medic", "Firebat", "Marauder", "Reaper", "Ghost", "Spectre",
|
||||
"Hellion", "Vulture", "Goliath", "Diamondback", "Siege Tank", "Thor",
|
||||
"Medivac", "Wraith", "Viking", "Banshee", "Battlecruiser",
|
||||
"Bunker", "Missile Turret"
|
||||
]
|
||||
|
||||
BARRACKS_UNITS = {"Marine", "Medic", "Firebat", "Marauder", "Reaper", "Ghost", "Spectre"}
|
||||
FACTORY_UNITS = {"Hellion", "Vulture", "Goliath", "Diamondback", "Siege Tank", "Thor", "Predator"}
|
||||
STARPORT_UNITS = {"Medivac", "Wraith", "Viking", "Banshee", "Battlecruiser", "Hercules", "Science Vessel", "Raven"}
|
||||
|
||||
PROTOSS_REGIONS = {"A Sinister Turn", "Echoes of the Future", "In Utter Darkness"}
|
||||
|
||||
|
||||
def filter_missions(world: MultiWorld, player: int) -> Dict[str, List[str]]:
|
||||
"""
|
||||
Returns a semi-randomly pruned tuple of no-build, easy, medium, and hard mission sets
|
||||
"""
|
||||
|
||||
mission_order_type = get_option_value(world, player, "mission_order")
|
||||
shuffle_protoss = get_option_value(world, player, "shuffle_protoss")
|
||||
excluded_missions = set(get_option_set_value(world, player, "excluded_missions"))
|
||||
invalid_mission_names = excluded_missions.difference(vanilla_mission_req_table.keys())
|
||||
if invalid_mission_names:
|
||||
raise Exception("Error in locked_missions - the following are not valid mission names: " + ", ".join(invalid_mission_names))
|
||||
mission_count = len(mission_orders[mission_order_type]) - 1
|
||||
# Vanilla and Vanilla Shuffled use the entire mission pool
|
||||
if mission_count == 28:
|
||||
return {
|
||||
"no_build": no_build_regions_list[:],
|
||||
"easy": easy_regions_list[:],
|
||||
"medium": medium_regions_list[:],
|
||||
"hard": hard_regions_list[:],
|
||||
"all_in": ["All-In"]
|
||||
}
|
||||
|
||||
mission_pools = [
|
||||
[],
|
||||
easy_regions_list,
|
||||
medium_regions_list,
|
||||
hard_regions_list
|
||||
]
|
||||
# Omitting Protoss missions if not shuffling protoss
|
||||
if not shuffle_protoss:
|
||||
excluded_missions = excluded_missions.union(PROTOSS_REGIONS)
|
||||
# Replacing All-In on low mission counts
|
||||
if mission_count < 14:
|
||||
final_mission = world.random.choice([mission for mission in alt_final_mission_locations.keys() if mission not in excluded_missions])
|
||||
excluded_missions.add(final_mission)
|
||||
else:
|
||||
final_mission = 'All-In'
|
||||
# Yaml settings determine which missions can be placed in the first slot
|
||||
mission_pools[0] = [mission for mission in get_starting_mission_locations(world, player).keys() if mission not in excluded_missions]
|
||||
# Removing the new no-build missions from their original sets
|
||||
for i in range(1, len(mission_pools)):
|
||||
mission_pools[i] = [mission for mission in mission_pools[i] if mission not in excluded_missions.union(mission_pools[0])]
|
||||
# If the first mission is a build mission, there may not be enough locations to reach Outbreak as a second mission
|
||||
if not get_option_value(world, player, 'shuffle_no_build'):
|
||||
# Swapping Outbreak and The Great Train Robbery
|
||||
if "Outbreak" in mission_pools[1]:
|
||||
mission_pools[1].remove("Outbreak")
|
||||
mission_pools[2].append("Outbreak")
|
||||
if "The Great Train Robbery" in mission_pools[2]:
|
||||
mission_pools[2].remove("The Great Train Robbery")
|
||||
mission_pools[1].append("The Great Train Robbery")
|
||||
# Removing random missions from each difficulty set in a cycle
|
||||
set_cycle = 0
|
||||
current_count = sum(len(mission_pool) for mission_pool in mission_pools)
|
||||
|
||||
if current_count < mission_count:
|
||||
raise Exception("Not enough missions available to fill the campaign on current settings. Please exclude fewer missions.")
|
||||
while current_count > mission_count:
|
||||
if set_cycle == 4:
|
||||
set_cycle = 0
|
||||
# Must contain at least one mission per set
|
||||
mission_pool = mission_pools[set_cycle]
|
||||
if len(mission_pool) <= 1:
|
||||
if all(len(mission_pool) <= 1 for mission_pool in mission_pools):
|
||||
raise Exception("Not enough missions available to fill the campaign on current settings. Please exclude fewer missions.")
|
||||
else:
|
||||
mission_pool.remove(world.random.choice(mission_pool))
|
||||
current_count -= 1
|
||||
set_cycle += 1
|
||||
|
||||
return {
|
||||
"no_build": mission_pools[0],
|
||||
"easy": mission_pools[1],
|
||||
"medium": mission_pools[2],
|
||||
"hard": mission_pools[3],
|
||||
"all_in": [final_mission]
|
||||
}
|
||||
|
||||
|
||||
def get_item_upgrades(inventory: List[Item], parent_item: Item or str):
|
||||
item_name = parent_item.name if isinstance(parent_item, Item) else parent_item
|
||||
return [
|
||||
inv_item for inv_item in inventory
|
||||
if item_table[inv_item.name].parent_item == item_name
|
||||
]
|
||||
|
||||
|
||||
class ValidInventory:
|
||||
|
||||
def has(self, item: str, player: int):
|
||||
return item in self.logical_inventory
|
||||
|
||||
def has_any(self, items: Set[str], player: int):
|
||||
return any(item in self.logical_inventory for item in items)
|
||||
|
||||
def has_all(self, items: Set[str], player: int):
|
||||
return all(item in self.logical_inventory for item in items)
|
||||
|
||||
def has_units_per_structure(self) -> bool:
|
||||
return len(BARRACKS_UNITS.intersection(self.logical_inventory)) > self.min_units_per_structure and \
|
||||
len(FACTORY_UNITS.intersection(self.logical_inventory)) > self.min_units_per_structure and \
|
||||
len(STARPORT_UNITS.intersection(self.logical_inventory)) > self.min_units_per_structure
|
||||
|
||||
def generate_reduced_inventory(self, inventory_size: int, mission_requirements: List[Callable]) -> List[Item]:
|
||||
"""Attempts to generate a reduced inventory that can fulfill the mission requirements."""
|
||||
inventory = list(self.item_pool)
|
||||
locked_items = list(self.locked_items)
|
||||
self.logical_inventory = {
|
||||
item.name for item in inventory + locked_items + self.existing_items
|
||||
if item.classification in (ItemClassification.progression, ItemClassification.progression_skip_balancing)
|
||||
}
|
||||
requirements = mission_requirements
|
||||
cascade_keys = self.cascade_removal_map.keys()
|
||||
units_always_have_upgrades = get_option_value(self.world, self.player, "units_always_have_upgrades")
|
||||
if self.min_units_per_structure > 0:
|
||||
requirements.append(lambda state: state.has_units_per_structure())
|
||||
|
||||
def attempt_removal(item: Item) -> bool:
|
||||
# If item can be removed and has associated items, remove them as well
|
||||
inventory.remove(item)
|
||||
# Only run logic checks when removing logic items
|
||||
if item.name in self.logical_inventory:
|
||||
self.logical_inventory.remove(item.name)
|
||||
if not all(requirement(self) for requirement in requirements):
|
||||
# If item cannot be removed, lock or revert
|
||||
self.logical_inventory.add(item.name)
|
||||
locked_items.append(item)
|
||||
return False
|
||||
return True
|
||||
|
||||
while len(inventory) + len(locked_items) > inventory_size:
|
||||
if len(inventory) == 0:
|
||||
raise Exception("Reduced item pool generation failed - not enough locations available to place items.")
|
||||
# Select random item from removable items
|
||||
item = self.world.random.choice(inventory)
|
||||
# Cascade removals to associated items
|
||||
if item in cascade_keys:
|
||||
items_to_remove = self.cascade_removal_map[item]
|
||||
transient_items = []
|
||||
while len(items_to_remove) > 0:
|
||||
item_to_remove = items_to_remove.pop()
|
||||
if item_to_remove not in inventory:
|
||||
continue
|
||||
success = attempt_removal(item_to_remove)
|
||||
if success:
|
||||
transient_items.append(item_to_remove)
|
||||
elif units_always_have_upgrades:
|
||||
# Lock all associated items if any of them cannot be removed
|
||||
transient_items += items_to_remove
|
||||
for transient_item in transient_items:
|
||||
if transient_item not in inventory and transient_item not in locked_items:
|
||||
locked_items += transient_item
|
||||
if transient_item.classification in (ItemClassification.progression, ItemClassification.progression_skip_balancing):
|
||||
self.logical_inventory.add(transient_item.name)
|
||||
break
|
||||
else:
|
||||
attempt_removal(item)
|
||||
|
||||
return inventory + locked_items
|
||||
|
||||
def _read_logic(self):
|
||||
self._sc2wol_has_common_unit = lambda world, player: SC2WoLLogic._sc2wol_has_common_unit(self, world, player)
|
||||
self._sc2wol_has_air = lambda world, player: SC2WoLLogic._sc2wol_has_air(self, world, player)
|
||||
self._sc2wol_has_air_anti_air = lambda world, player: SC2WoLLogic._sc2wol_has_air_anti_air(self, world, player)
|
||||
self._sc2wol_has_competent_anti_air = lambda world, player: SC2WoLLogic._sc2wol_has_competent_anti_air(self, world, player)
|
||||
self._sc2wol_has_anti_air = lambda world, player: SC2WoLLogic._sc2wol_has_anti_air(self, world, player)
|
||||
self._sc2wol_defense_rating = lambda world, player, zerg_enemy, air_enemy=False: SC2WoLLogic._sc2wol_defense_rating(self, world, player, zerg_enemy, air_enemy)
|
||||
self._sc2wol_has_competent_comp = lambda world, player: SC2WoLLogic._sc2wol_has_competent_comp(self, world, player)
|
||||
self._sc2wol_has_train_killers = lambda world, player: SC2WoLLogic._sc2wol_has_train_killers(self, world, player)
|
||||
self._sc2wol_able_to_rescue = lambda world, player: SC2WoLLogic._sc2wol_able_to_rescue(self, world, player)
|
||||
self._sc2wol_beats_protoss_deathball = lambda world, player: SC2WoLLogic._sc2wol_beats_protoss_deathball(self, world, player)
|
||||
self._sc2wol_survives_rip_field = lambda world, player: SC2WoLLogic._sc2wol_survives_rip_field(self, world, player)
|
||||
self._sc2wol_has_protoss_common_units = lambda world, player: SC2WoLLogic._sc2wol_has_protoss_common_units(self, world, player)
|
||||
self._sc2wol_has_protoss_medium_units = lambda world, player: SC2WoLLogic._sc2wol_has_protoss_medium_units(self, world, player)
|
||||
self._sc2wol_has_mm_upgrade = lambda world, player: SC2WoLLogic._sc2wol_has_mm_upgrade(self, world, player)
|
||||
self._sc2wol_final_mission_requirements = lambda world, player: SC2WoLLogic._sc2wol_final_mission_requirements(self, world, player)
|
||||
|
||||
def __init__(self, world: MultiWorld, player: int,
|
||||
item_pool: List[Item], existing_items: List[Item], locked_items: List[Item],
|
||||
has_protoss: bool):
|
||||
self.world = world
|
||||
self.player = player
|
||||
self.logical_inventory = set()
|
||||
self.locked_items = locked_items[:]
|
||||
self.existing_items = existing_items
|
||||
self._read_logic()
|
||||
# Initial filter of item pool
|
||||
self.item_pool = []
|
||||
item_quantities: dict[str, int] = dict()
|
||||
# Inventory restrictiveness based on number of missions with checks
|
||||
mission_order_type = get_option_value(self.world, self.player, "mission_order")
|
||||
mission_count = len(mission_orders[mission_order_type]) - 1
|
||||
self.min_units_per_structure = int(mission_count / 7)
|
||||
min_upgrades = 1 if mission_count < 10 else 2
|
||||
for item in item_pool:
|
||||
item_info = item_table[item.name]
|
||||
if item_info.type == "Upgrade":
|
||||
# Locking upgrades based on mission duration
|
||||
if item.name not in item_quantities:
|
||||
item_quantities[item.name] = 0
|
||||
item_quantities[item.name] += 1
|
||||
if item_quantities[item.name] < min_upgrades:
|
||||
self.locked_items.append(item)
|
||||
else:
|
||||
self.item_pool.append(item)
|
||||
elif item_info.type == "Goal":
|
||||
locked_items.append(item)
|
||||
elif item_info.type != "Protoss" or has_protoss:
|
||||
self.item_pool.append(item)
|
||||
self.cascade_removal_map: Dict[Item, List[Item]] = dict()
|
||||
for item in self.item_pool + locked_items + existing_items:
|
||||
if item.name in UPGRADABLE_ITEMS:
|
||||
upgrades = get_item_upgrades(self.item_pool, item)
|
||||
associated_items = [*upgrades, item]
|
||||
self.cascade_removal_map[item] = associated_items
|
||||
if get_option_value(world, player, "units_always_have_upgrades"):
|
||||
for upgrade in upgrades:
|
||||
self.cascade_removal_map[upgrade] = associated_items
|
||||
|
||||
|
||||
def filter_items(world: MultiWorld, player: int, mission_req_table: Dict[str, MissionInfo], location_cache: List[Location],
|
||||
item_pool: List[Item], existing_items: List[Item], locked_items: List[Item]) -> List[Item]:
|
||||
"""
|
||||
Returns a semi-randomly pruned set of items based on number of available locations.
|
||||
The returned inventory must be capable of logically accessing every location in the world.
|
||||
"""
|
||||
open_locations = [location for location in location_cache if location.item is None]
|
||||
inventory_size = len(open_locations)
|
||||
has_protoss = bool(PROTOSS_REGIONS.intersection(mission_req_table.keys()))
|
||||
mission_requirements = [location.access_rule for location in location_cache]
|
||||
valid_inventory = ValidInventory(world, player, item_pool, existing_items, locked_items, has_protoss)
|
||||
|
||||
valid_items = valid_inventory.generate_reduced_inventory(inventory_size, mission_requirements)
|
||||
return valid_items
|
||||
@@ -2,55 +2,47 @@ from typing import List, Set, Dict, Tuple, Optional, Callable
|
||||
from BaseClasses import MultiWorld, Region, Entrance, Location, RegionType
|
||||
from .Locations import LocationData
|
||||
from .Options import get_option_value
|
||||
from .MissionTables import MissionInfo, vanilla_shuffle_order, vanilla_mission_req_table, \
|
||||
no_build_regions_list, easy_regions_list, medium_regions_list, hard_regions_list
|
||||
from .MissionTables import MissionInfo, mission_orders, vanilla_mission_req_table, alt_final_mission_locations
|
||||
from .PoolFilter import filter_missions
|
||||
import random
|
||||
|
||||
|
||||
def create_regions(world: MultiWorld, player: int, locations: Tuple[LocationData, ...], location_cache: List[Location]):
|
||||
def create_regions(world: MultiWorld, player: int, locations: Tuple[LocationData, ...], location_cache: List[Location])\
|
||||
-> Tuple[Dict[str, MissionInfo], int, str]:
|
||||
locations_per_region = get_locations_per_region(locations)
|
||||
|
||||
regions = [
|
||||
create_region(world, player, locations_per_region, location_cache, "Menu"),
|
||||
create_region(world, player, locations_per_region, location_cache, "Liberation Day"),
|
||||
create_region(world, player, locations_per_region, location_cache, "The Outlaws"),
|
||||
create_region(world, player, locations_per_region, location_cache, "Zero Hour"),
|
||||
create_region(world, player, locations_per_region, location_cache, "Evacuation"),
|
||||
create_region(world, player, locations_per_region, location_cache, "Outbreak"),
|
||||
create_region(world, player, locations_per_region, location_cache, "Safe Haven"),
|
||||
create_region(world, player, locations_per_region, location_cache, "Haven's Fall"),
|
||||
create_region(world, player, locations_per_region, location_cache, "Smash and Grab"),
|
||||
create_region(world, player, locations_per_region, location_cache, "The Dig"),
|
||||
create_region(world, player, locations_per_region, location_cache, "The Moebius Factor"),
|
||||
create_region(world, player, locations_per_region, location_cache, "Supernova"),
|
||||
create_region(world, player, locations_per_region, location_cache, "Maw of the Void"),
|
||||
create_region(world, player, locations_per_region, location_cache, "Devil's Playground"),
|
||||
create_region(world, player, locations_per_region, location_cache, "Welcome to the Jungle"),
|
||||
create_region(world, player, locations_per_region, location_cache, "Breakout"),
|
||||
create_region(world, player, locations_per_region, location_cache, "Ghost of a Chance"),
|
||||
create_region(world, player, locations_per_region, location_cache, "The Great Train Robbery"),
|
||||
create_region(world, player, locations_per_region, location_cache, "Cutthroat"),
|
||||
create_region(world, player, locations_per_region, location_cache, "Engine of Destruction"),
|
||||
create_region(world, player, locations_per_region, location_cache, "Media Blitz"),
|
||||
create_region(world, player, locations_per_region, location_cache, "Piercing the Shroud"),
|
||||
create_region(world, player, locations_per_region, location_cache, "Whispers of Doom"),
|
||||
create_region(world, player, locations_per_region, location_cache, "A Sinister Turn"),
|
||||
create_region(world, player, locations_per_region, location_cache, "Echoes of the Future"),
|
||||
create_region(world, player, locations_per_region, location_cache, "In Utter Darkness"),
|
||||
create_region(world, player, locations_per_region, location_cache, "Gates of Hell"),
|
||||
create_region(world, player, locations_per_region, location_cache, "Belly of the Beast"),
|
||||
create_region(world, player, locations_per_region, location_cache, "Shatter the Sky"),
|
||||
create_region(world, player, locations_per_region, location_cache, "All-In")
|
||||
]
|
||||
mission_order_type = get_option_value(world, player, "mission_order")
|
||||
mission_order = mission_orders[mission_order_type]
|
||||
|
||||
mission_pools = filter_missions(world, player)
|
||||
final_mission = mission_pools['all_in'][0]
|
||||
|
||||
used_regions = [mission for mission_pool in mission_pools.values() for mission in mission_pool]
|
||||
regions = [create_region(world, player, locations_per_region, location_cache, "Menu")]
|
||||
for region_name in used_regions:
|
||||
regions.append(create_region(world, player, locations_per_region, location_cache, region_name))
|
||||
# Changing the completion condition for alternate final missions into an event
|
||||
if final_mission != 'All-In':
|
||||
final_location = alt_final_mission_locations[final_mission]
|
||||
# Final location should be near the end of the cache
|
||||
for i in range(len(location_cache) - 1, -1, -1):
|
||||
if location_cache[i].name == final_location:
|
||||
location_cache[i].locked = True
|
||||
location_cache[i].event = True
|
||||
location_cache[i].address = None
|
||||
break
|
||||
else:
|
||||
final_location = 'All-In: Victory'
|
||||
|
||||
if __debug__:
|
||||
throwIfAnyLocationIsNotAssignedToARegion(regions, locations_per_region.keys())
|
||||
if mission_order_type in (0, 1):
|
||||
throwIfAnyLocationIsNotAssignedToARegion(regions, locations_per_region.keys())
|
||||
|
||||
world.regions += regions
|
||||
|
||||
names: Dict[str, int] = {}
|
||||
|
||||
if get_option_value(world, player, "mission_order") == 0:
|
||||
if mission_order_type == 0:
|
||||
connect(world, player, names, 'Menu', 'Liberation Day'),
|
||||
connect(world, player, names, 'Liberation Day', 'The Outlaws',
|
||||
lambda state: state.has("Beat Liberation Day", player)),
|
||||
@@ -119,32 +111,30 @@ def create_regions(world: MultiWorld, player: int, locations: Tuple[LocationData
|
||||
lambda state: state.has('Beat Gates of Hell', player) and (
|
||||
state.has('Beat Shatter the Sky', player) or state.has('Beat Belly of the Beast', player)))
|
||||
|
||||
return vanilla_mission_req_table
|
||||
return vanilla_mission_req_table, 29, final_location
|
||||
|
||||
elif get_option_value(world, player, "mission_order") == 1:
|
||||
else:
|
||||
missions = []
|
||||
no_build_pool = no_build_regions_list[:]
|
||||
easy_pool = easy_regions_list[:]
|
||||
medium_pool = medium_regions_list[:]
|
||||
hard_pool = hard_regions_list[:]
|
||||
|
||||
# Initial fill out of mission list and marking all-in mission
|
||||
for mission in vanilla_shuffle_order:
|
||||
if mission.type == "all_in":
|
||||
missions.append("All-In")
|
||||
elif get_option_value(world, player, "relegate_no_build") and mission.relegate:
|
||||
for mission in mission_order:
|
||||
if mission is None:
|
||||
missions.append(None)
|
||||
elif mission.type == "all_in":
|
||||
missions.append(final_mission)
|
||||
elif mission.relegate and not get_option_value(world, player, "shuffle_no_build"):
|
||||
missions.append("no_build")
|
||||
else:
|
||||
missions.append(mission.type)
|
||||
|
||||
# Place Protoss Missions if we are not using ShuffleProtoss
|
||||
if get_option_value(world, player, "shuffle_protoss") == 0:
|
||||
# Place Protoss Missions if we are not using ShuffleProtoss and are in Vanilla Shuffled
|
||||
if get_option_value(world, player, "shuffle_protoss") == 0 and mission_order_type == 1:
|
||||
missions[22] = "A Sinister Turn"
|
||||
medium_pool.remove("A Sinister Turn")
|
||||
mission_pools['medium'].remove("A Sinister Turn")
|
||||
missions[23] = "Echoes of the Future"
|
||||
medium_pool.remove("Echoes of the Future")
|
||||
mission_pools['medium'].remove("Echoes of the Future")
|
||||
missions[24] = "In Utter Darkness"
|
||||
hard_pool.remove("In Utter Darkness")
|
||||
mission_pools['hard'].remove("In Utter Darkness")
|
||||
|
||||
no_build_slots = []
|
||||
easy_slots = []
|
||||
@@ -153,6 +143,8 @@ def create_regions(world: MultiWorld, player: int, locations: Tuple[LocationData
|
||||
|
||||
# Search through missions to find slots needed to fill
|
||||
for i in range(len(missions)):
|
||||
if missions[i] is None:
|
||||
continue
|
||||
if missions[i] == "no_build":
|
||||
no_build_slots.append(i)
|
||||
elif missions[i] == "easy":
|
||||
@@ -163,30 +155,30 @@ def create_regions(world: MultiWorld, player: int, locations: Tuple[LocationData
|
||||
hard_slots.append(i)
|
||||
|
||||
# Add no_build missions to the pool and fill in no_build slots
|
||||
missions_to_add = no_build_pool
|
||||
missions_to_add = mission_pools['no_build']
|
||||
for slot in no_build_slots:
|
||||
filler = random.randint(0, len(missions_to_add)-1)
|
||||
filler = world.random.randint(0, len(missions_to_add)-1)
|
||||
|
||||
missions[slot] = missions_to_add.pop(filler)
|
||||
|
||||
# Add easy missions into pool and fill in easy slots
|
||||
missions_to_add = missions_to_add + easy_pool
|
||||
missions_to_add = missions_to_add + mission_pools['easy']
|
||||
for slot in easy_slots:
|
||||
filler = random.randint(0, len(missions_to_add) - 1)
|
||||
filler = world.random.randint(0, len(missions_to_add) - 1)
|
||||
|
||||
missions[slot] = missions_to_add.pop(filler)
|
||||
|
||||
# Add medium missions into pool and fill in medium slots
|
||||
missions_to_add = missions_to_add + medium_pool
|
||||
missions_to_add = missions_to_add + mission_pools['medium']
|
||||
for slot in medium_slots:
|
||||
filler = random.randint(0, len(missions_to_add) - 1)
|
||||
filler = world.random.randint(0, len(missions_to_add) - 1)
|
||||
|
||||
missions[slot] = missions_to_add.pop(filler)
|
||||
|
||||
# Add hard missions into pool and fill in hard slots
|
||||
missions_to_add = missions_to_add + hard_pool
|
||||
missions_to_add = missions_to_add + mission_pools['hard']
|
||||
for slot in hard_slots:
|
||||
filler = random.randint(0, len(missions_to_add) - 1)
|
||||
filler = world.random.randint(0, len(missions_to_add) - 1)
|
||||
|
||||
missions[slot] = missions_to_add.pop(filler)
|
||||
|
||||
@@ -195,7 +187,7 @@ def create_regions(world: MultiWorld, player: int, locations: Tuple[LocationData
|
||||
mission_req_table = {}
|
||||
for i in range(len(missions)):
|
||||
connections = []
|
||||
for connection in vanilla_shuffle_order[i].connect_to:
|
||||
for connection in mission_order[i].connect_to:
|
||||
if connection == -1:
|
||||
connect(world, player, names, "Menu", missions[i])
|
||||
else:
|
||||
@@ -203,16 +195,17 @@ def create_regions(world: MultiWorld, player: int, locations: Tuple[LocationData
|
||||
(lambda name, missions_req: (lambda state: state.has(f"Beat {name}", player) and
|
||||
state._sc2wol_cleared_missions(world, player,
|
||||
missions_req)))
|
||||
(missions[connection], vanilla_shuffle_order[i].number))
|
||||
(missions[connection], mission_order[i].number))
|
||||
connections.append(connection + 1)
|
||||
|
||||
mission_req_table.update({missions[i]: MissionInfo(
|
||||
vanilla_mission_req_table[missions[i]].id, vanilla_mission_req_table[missions[i]].extra_locations,
|
||||
connections, vanilla_shuffle_order[i].category, number=vanilla_shuffle_order[i].number,
|
||||
completion_critical=vanilla_shuffle_order[i].completion_critical,
|
||||
or_requirements=vanilla_shuffle_order[i].or_requirements)})
|
||||
vanilla_mission_req_table[missions[i]].id, connections, mission_order[i].category,
|
||||
number=mission_order[i].number,
|
||||
completion_critical=mission_order[i].completion_critical,
|
||||
or_requirements=mission_order[i].or_requirements)})
|
||||
|
||||
return mission_req_table
|
||||
final_mission_id = vanilla_mission_req_table[final_mission].id
|
||||
return mission_req_table, final_mission_id, final_mission + ': Victory'
|
||||
|
||||
|
||||
def throwIfAnyLocationIsNotAssignedToARegion(regions: List[Region], regionNames: Set[str]):
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import typing
|
||||
|
||||
from typing import List, Set, Tuple
|
||||
from typing import List, Set, Tuple, Dict
|
||||
from BaseClasses import Item, MultiWorld, Location, Tutorial, ItemClassification
|
||||
from worlds.AutoWorld import WebWorld, World
|
||||
from .Items import StarcraftWoLItem, item_table, filler_items, item_name_groups, get_full_item_list, \
|
||||
basic_unit
|
||||
get_basic_units
|
||||
from .Locations import get_locations
|
||||
from .Regions import create_regions
|
||||
from .Options import sc2wol_options, get_option_value
|
||||
from .Options import sc2wol_options, get_option_value, get_option_set_value
|
||||
from .LogicMixin import SC2WoLLogic
|
||||
from .PoolFilter import filter_missions, filter_items, get_item_upgrades
|
||||
from .MissionTables import get_starting_mission_locations, MissionInfo
|
||||
|
||||
|
||||
class Starcraft2WoLWebWorld(WebWorld):
|
||||
@@ -42,6 +44,8 @@ class SC2WoLWorld(World):
|
||||
locked_locations: typing.List[str]
|
||||
location_cache: typing.List[Location]
|
||||
mission_req_table = {}
|
||||
final_mission_id: int
|
||||
victory_item: str
|
||||
required_client_version = 0, 3, 5
|
||||
|
||||
def __init__(self, world: MultiWorld, player: int):
|
||||
@@ -49,24 +53,21 @@ class SC2WoLWorld(World):
|
||||
self.location_cache = []
|
||||
self.locked_locations = []
|
||||
|
||||
def _create_items(self, name: str):
|
||||
data = get_full_item_list()[name]
|
||||
return [self.create_item(name) for _ in range(data.quantity)]
|
||||
|
||||
def create_item(self, name: str) -> Item:
|
||||
data = get_full_item_list()[name]
|
||||
return StarcraftWoLItem(name, data.classification, data.code, self.player)
|
||||
|
||||
def create_regions(self):
|
||||
self.mission_req_table = create_regions(self.world, self.player, get_locations(self.world, self.player),
|
||||
self.location_cache)
|
||||
self.mission_req_table, self.final_mission_id, self.victory_item = create_regions(
|
||||
self.world, self.player, get_locations(self.world, self.player), self.location_cache
|
||||
)
|
||||
|
||||
def generate_basic(self):
|
||||
excluded_items = get_excluded_items(self, self.world, self.player)
|
||||
|
||||
assign_starter_items(self.world, self.player, excluded_items, self.locked_locations)
|
||||
starter_items = assign_starter_items(self.world, self.player, excluded_items, self.locked_locations)
|
||||
|
||||
pool = get_item_pool(self.world, self.player, excluded_items)
|
||||
pool = get_item_pool(self.world, self.player, self.mission_req_table, starter_items, excluded_items, self.location_cache)
|
||||
|
||||
fill_item_pool_with_dummy_items(self, self.world, self.player, self.locked_locations, self.location_cache, pool)
|
||||
|
||||
@@ -74,8 +75,7 @@ class SC2WoLWorld(World):
|
||||
|
||||
def set_rules(self):
|
||||
setup_events(self.world, self.player, self.locked_locations, self.location_cache)
|
||||
|
||||
self.world.completion_condition[self.player] = lambda state: state.has('All-In: Victory', self.player)
|
||||
self.world.completion_condition[self.player] = lambda state: state.has(self.victory_item, self.player)
|
||||
|
||||
def get_filler_item_name(self) -> str:
|
||||
return self.world.random.choice(filler_items)
|
||||
@@ -91,6 +91,7 @@ class SC2WoLWorld(World):
|
||||
slot_req_table[mission] = self.mission_req_table[mission]._asdict()
|
||||
|
||||
slot_data["mission_req"] = slot_req_table
|
||||
slot_data["final_mission"] = self.final_mission_id
|
||||
return slot_data
|
||||
|
||||
|
||||
@@ -120,30 +121,37 @@ def get_excluded_items(self: SC2WoLWorld, world: MultiWorld, player: int) -> Set
|
||||
for item in world.precollected_items[player]:
|
||||
excluded_items.add(item.name)
|
||||
|
||||
excluded_items_option = getattr(world, 'excluded_items', [])
|
||||
|
||||
excluded_items.update(excluded_items_option[player].value)
|
||||
|
||||
return excluded_items
|
||||
|
||||
|
||||
def assign_starter_items(world: MultiWorld, player: int, excluded_items: Set[str], locked_locations: List[str]):
|
||||
def assign_starter_items(world: MultiWorld, player: int, excluded_items: Set[str], locked_locations: List[str]) -> List[Item]:
|
||||
non_local_items = world.non_local_items[player].value
|
||||
if get_option_value(world, player, "early_unit"):
|
||||
local_basic_unit = tuple(item for item in get_basic_units(world, player) if item not in non_local_items)
|
||||
if not local_basic_unit:
|
||||
raise Exception("At least one basic unit must be local")
|
||||
|
||||
local_basic_unit = tuple(item for item in basic_unit if item not in non_local_items)
|
||||
if not local_basic_unit:
|
||||
raise Exception("At least one basic unit must be local")
|
||||
# The first world should also be the starting world
|
||||
first_mission = list(world.worlds[player].mission_req_table)[0]
|
||||
starting_mission_locations = get_starting_mission_locations(world, player)
|
||||
if first_mission in starting_mission_locations:
|
||||
first_location = starting_mission_locations[first_mission]
|
||||
elif first_mission == "In Utter Darkness":
|
||||
first_location = first_mission + ": Defeat"
|
||||
else:
|
||||
first_location = first_mission + ": Victory"
|
||||
|
||||
# The first world should also be the starting world
|
||||
first_location = list(world.worlds[player].mission_req_table)[0]
|
||||
|
||||
if first_location == "In Utter Darkness":
|
||||
first_location = first_location + ": Defeat"
|
||||
return [assign_starter_item(world, player, excluded_items, locked_locations, first_location, local_basic_unit)]
|
||||
else:
|
||||
first_location = first_location + ": Victory"
|
||||
|
||||
assign_starter_item(world, player, excluded_items, locked_locations, first_location,
|
||||
local_basic_unit)
|
||||
return []
|
||||
|
||||
|
||||
def assign_starter_item(world: MultiWorld, player: int, excluded_items: Set[str], locked_locations: List[str],
|
||||
location: str, item_list: Tuple[str, ...]):
|
||||
location: str, item_list: Tuple[str, ...]) -> Item:
|
||||
|
||||
item_name = world.random.choice(item_list)
|
||||
|
||||
@@ -155,17 +163,40 @@ def assign_starter_item(world: MultiWorld, player: int, excluded_items: Set[str]
|
||||
|
||||
locked_locations.append(location)
|
||||
|
||||
return item
|
||||
|
||||
def get_item_pool(world: MultiWorld, player: int, excluded_items: Set[str]) -> List[Item]:
|
||||
|
||||
def get_item_pool(world: MultiWorld, player: int, mission_req_table: Dict[str, MissionInfo],
|
||||
starter_items: List[str], excluded_items: Set[str], location_cache: List[Location]) -> List[Item]:
|
||||
pool: List[Item] = []
|
||||
|
||||
# For the future: goal items like Artifact Shards go here
|
||||
locked_items = []
|
||||
|
||||
# YAML items
|
||||
yaml_locked_items = get_option_set_value(world, player, 'locked_items')
|
||||
|
||||
for name, data in item_table.items():
|
||||
if name not in excluded_items:
|
||||
for _ in range(data.quantity):
|
||||
item = create_item_with_correct_settings(world, player, name)
|
||||
pool.append(item)
|
||||
if name in yaml_locked_items:
|
||||
locked_items.append(item)
|
||||
else:
|
||||
pool.append(item)
|
||||
|
||||
return pool
|
||||
existing_items = starter_items + [item for item in world.precollected_items[player]]
|
||||
existing_names = [item.name for item in existing_items]
|
||||
# Removing upgrades for excluded items
|
||||
for item_name in excluded_items:
|
||||
if item_name in existing_names:
|
||||
continue
|
||||
invalid_upgrades = get_item_upgrades(pool, item_name)
|
||||
for invalid_upgrade in invalid_upgrades:
|
||||
pool.remove(invalid_upgrade)
|
||||
|
||||
filtered_pool = filter_items(world, player, mission_req_table, location_cache, pool, existing_items, locked_items)
|
||||
return filtered_pool
|
||||
|
||||
|
||||
def fill_item_pool_with_dummy_items(self: SC2WoLWorld, world: MultiWorld, player: int, locked_locations: List[str],
|
||||
|
||||
158
worlds/sm/Client.py
Normal file
158
worlds/sm/Client.py
Normal file
@@ -0,0 +1,158 @@
|
||||
import logging
|
||||
import asyncio
|
||||
import time
|
||||
|
||||
from NetUtils import ClientStatus, color
|
||||
from worlds.AutoSNIClient import SNIClient
|
||||
from .Rom import ROM_PLAYER_LIMIT as SM_ROM_PLAYER_LIMIT
|
||||
|
||||
snes_logger = logging.getLogger("SNES")
|
||||
|
||||
GAME_SM = "Super Metroid"
|
||||
|
||||
# FXPAK Pro protocol memory mapping used by SNI
|
||||
ROM_START = 0x000000
|
||||
WRAM_START = 0xF50000
|
||||
WRAM_SIZE = 0x20000
|
||||
SRAM_START = 0xE00000
|
||||
|
||||
# SM
|
||||
SM_ROMNAME_START = ROM_START + 0x007FC0
|
||||
ROMNAME_SIZE = 0x15
|
||||
|
||||
SM_INGAME_MODES = {0x07, 0x09, 0x0b}
|
||||
SM_ENDGAME_MODES = {0x26, 0x27}
|
||||
SM_DEATH_MODES = {0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A}
|
||||
|
||||
# RECV and SEND are from the gameplay's perspective: SNIClient writes to RECV queue and reads from SEND queue
|
||||
SM_RECV_QUEUE_START = SRAM_START + 0x2000
|
||||
SM_RECV_QUEUE_WCOUNT = SRAM_START + 0x2602
|
||||
SM_SEND_QUEUE_START = SRAM_START + 0x2700
|
||||
SM_SEND_QUEUE_RCOUNT = SRAM_START + 0x2680
|
||||
SM_SEND_QUEUE_WCOUNT = SRAM_START + 0x2682
|
||||
|
||||
SM_DEATH_LINK_ACTIVE_ADDR = ROM_START + 0x277F04 # 1 byte
|
||||
SM_REMOTE_ITEM_FLAG_ADDR = ROM_START + 0x277F06 # 1 byte
|
||||
|
||||
|
||||
class SMSNIClient(SNIClient):
|
||||
game = "Super Metroid"
|
||||
|
||||
async def deathlink_kill_player(self, ctx):
|
||||
from SNIClient import DeathState, snes_buffered_write, snes_flush_writes, snes_read
|
||||
snes_buffered_write(ctx, WRAM_START + 0x09C2, bytes([1, 0])) # set current health to 1 (to prevent saving with 0 energy)
|
||||
snes_buffered_write(ctx, WRAM_START + 0x0A50, bytes([255])) # deal 255 of damage at next opportunity
|
||||
if not ctx.death_link_allow_survive:
|
||||
snes_buffered_write(ctx, WRAM_START + 0x09D6, bytes([0, 0])) # set current reserve to 0
|
||||
|
||||
await snes_flush_writes(ctx)
|
||||
await asyncio.sleep(1)
|
||||
|
||||
gamemode = await snes_read(ctx, WRAM_START + 0x0998, 1)
|
||||
health = await snes_read(ctx, WRAM_START + 0x09C2, 2)
|
||||
if health is not None:
|
||||
health = health[0] | (health[1] << 8)
|
||||
if not gamemode or gamemode[0] in SM_DEATH_MODES or (
|
||||
ctx.death_link_allow_survive and health is not None and health > 0):
|
||||
ctx.death_state = DeathState.dead
|
||||
|
||||
|
||||
async def validate_rom(self, ctx):
|
||||
from SNIClient import snes_buffered_write, snes_flush_writes, snes_read
|
||||
|
||||
rom_name = await snes_read(ctx, SM_ROMNAME_START, ROMNAME_SIZE)
|
||||
if rom_name is None or rom_name == bytes([0] * ROMNAME_SIZE) or rom_name[:2] != b"SM" or rom_name[:3] == b"SMW":
|
||||
return False
|
||||
|
||||
ctx.game = self.game
|
||||
|
||||
# versions lower than 0.3.0 dont have item handling flag nor remote item support
|
||||
romVersion = int(rom_name[2:5].decode('UTF-8'))
|
||||
if romVersion < 30:
|
||||
ctx.items_handling = 0b001 # full local
|
||||
else:
|
||||
item_handling = await snes_read(ctx, SM_REMOTE_ITEM_FLAG_ADDR, 1)
|
||||
ctx.items_handling = 0b001 if item_handling is None else item_handling[0]
|
||||
|
||||
ctx.rom = rom_name
|
||||
|
||||
death_link = await snes_read(ctx, SM_DEATH_LINK_ACTIVE_ADDR, 1)
|
||||
|
||||
if death_link:
|
||||
ctx.allow_collect = bool(death_link[0] & 0b100)
|
||||
ctx.death_link_allow_survive = bool(death_link[0] & 0b10)
|
||||
await ctx.update_death_link(bool(death_link[0] & 0b1))
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def game_watcher(self, ctx):
|
||||
from SNIClient import snes_buffered_write, snes_flush_writes, snes_read
|
||||
if ctx.server is None or ctx.slot is None:
|
||||
# not successfully connected to a multiworld server, cannot process the game sending items
|
||||
return
|
||||
|
||||
gamemode = await snes_read(ctx, WRAM_START + 0x0998, 1)
|
||||
if "DeathLink" in ctx.tags and gamemode and ctx.last_death_link + 1 < time.time():
|
||||
currently_dead = gamemode[0] in SM_DEATH_MODES
|
||||
await ctx.handle_deathlink_state(currently_dead)
|
||||
if gamemode is not None and gamemode[0] in SM_ENDGAME_MODES:
|
||||
if not ctx.finished_game:
|
||||
await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}])
|
||||
ctx.finished_game = True
|
||||
return
|
||||
|
||||
data = await snes_read(ctx, SM_SEND_QUEUE_RCOUNT, 4)
|
||||
if data is None:
|
||||
return
|
||||
|
||||
recv_index = data[0] | (data[1] << 8)
|
||||
recv_item = data[2] | (data[3] << 8) # this is actually SM_SEND_QUEUE_WCOUNT
|
||||
|
||||
while (recv_index < recv_item):
|
||||
item_address = recv_index * 8
|
||||
message = await snes_read(ctx, SM_SEND_QUEUE_START + item_address, 8)
|
||||
item_index = (message[4] | (message[5] << 8)) >> 3
|
||||
|
||||
recv_index += 1
|
||||
snes_buffered_write(ctx, SM_SEND_QUEUE_RCOUNT,
|
||||
bytes([recv_index & 0xFF, (recv_index >> 8) & 0xFF]))
|
||||
|
||||
from worlds.sm import locations_start_id
|
||||
location_id = locations_start_id + item_index
|
||||
|
||||
ctx.locations_checked.add(location_id)
|
||||
location = ctx.location_names[location_id]
|
||||
snes_logger.info(
|
||||
f'New Check: {location} ({len(ctx.locations_checked)}/{len(ctx.missing_locations) + len(ctx.checked_locations)})')
|
||||
await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": [location_id]}])
|
||||
|
||||
data = await snes_read(ctx, SM_RECV_QUEUE_WCOUNT, 2)
|
||||
if data is None:
|
||||
return
|
||||
|
||||
item_out_ptr = data[0] | (data[1] << 8)
|
||||
|
||||
from worlds.sm import items_start_id
|
||||
from worlds.sm import locations_start_id
|
||||
if item_out_ptr < len(ctx.items_received):
|
||||
item = ctx.items_received[item_out_ptr]
|
||||
item_id = item.item - items_start_id
|
||||
if bool(ctx.items_handling & 0b010):
|
||||
location_id = (item.location - locations_start_id) if (item.location >= 0 and item.player == ctx.slot) else 0xFF
|
||||
else:
|
||||
location_id = 0x00 #backward compat
|
||||
|
||||
player_id = item.player if item.player <= SM_ROM_PLAYER_LIMIT else 0
|
||||
snes_buffered_write(ctx, SM_RECV_QUEUE_START + item_out_ptr * 4, bytes(
|
||||
[player_id & 0xFF, (player_id >> 8) & 0xFF, item_id & 0xFF, location_id & 0xFF]))
|
||||
item_out_ptr += 1
|
||||
snes_buffered_write(ctx, SM_RECV_QUEUE_WCOUNT,
|
||||
bytes([item_out_ptr & 0xFF, (item_out_ptr >> 8) & 0xFF]))
|
||||
logging.info('Received %s from %s (%s) (%d/%d in list)' % (
|
||||
color(ctx.item_names[item.item], 'red', 'bold'),
|
||||
color(ctx.player_names[item.player], 'yellow'),
|
||||
ctx.location_names[item.location], item_out_ptr, len(ctx.items_received)))
|
||||
|
||||
await snes_flush_writes(ctx)
|
||||
|
||||
@@ -14,6 +14,7 @@ logger = logging.getLogger("Super Metroid")
|
||||
from .Regions import create_regions
|
||||
from .Rules import set_rules, add_entrance_rule
|
||||
from .Options import sm_options
|
||||
from .Client import SMSNIClient
|
||||
from .Rom import get_base_rom_path, ROM_PLAYER_LIMIT, SMDeltaPatch, get_sm_symbols
|
||||
import Utils
|
||||
|
||||
@@ -657,12 +658,6 @@ class SMWorld(World):
|
||||
loc.place_locked_item(item)
|
||||
loc.address = loc.item.code = None
|
||||
|
||||
@classmethod
|
||||
def stage_fill_hook(cls, world, progitempool, usefulitempool, filleritempool, fill_locations):
|
||||
if world.get_game_players("Super Metroid"):
|
||||
progitempool.sort(
|
||||
key=lambda item: 1 if (item.name == 'Morph Ball') else 0)
|
||||
|
||||
@classmethod
|
||||
def stage_post_fill(cls, world):
|
||||
new_state = CollectionState(world)
|
||||
|
||||
@@ -46,7 +46,7 @@ class MIPS1Cost(Range):
|
||||
|
||||
|
||||
class MIPS2Cost(Range):
|
||||
"""How many stars are required to spawn MIPS the secound time. Must be bigger or equal MIPS1Cost"""
|
||||
"""How many stars are required to spawn MIPS the second time."""
|
||||
range_start = 0
|
||||
range_end = 80
|
||||
default = 50
|
||||
@@ -72,7 +72,8 @@ class AreaRandomizer(Choice):
|
||||
display_name = "Entrance Randomizer"
|
||||
option_Off = 0
|
||||
option_Courses_Only = 1
|
||||
option_Courses_and_Secrets = 2
|
||||
option_Courses_and_Secrets_Separate = 2
|
||||
option_Courses_and_Secrets = 3
|
||||
|
||||
|
||||
class BuddyChecks(Toggle):
|
||||
|
||||
@@ -11,13 +11,14 @@ def fix_reg(entrance_ids, reg, invalidspot, swaplist, world):
|
||||
|
||||
def set_rules(world, player: int, area_connections):
|
||||
destination_regions = list(range(13)) + [12,13,14] + list(range(15,15+len(sm64secrets))) # Two instances of Destination Course THI. Past normal course idx are secret regions
|
||||
if world.AreaRandomizer[player].value == 0:
|
||||
entrance_ids = list(range(len(sm64paintings + sm64secrets)))
|
||||
if world.AreaRandomizer[player].value >= 1: # Some randomization is happening, randomize Courses
|
||||
entrance_ids = list(range(len(sm64paintings)))
|
||||
world.random.shuffle(entrance_ids)
|
||||
entrance_ids = entrance_ids + list(range(len(sm64paintings), len(sm64paintings) + len(sm64secrets)))
|
||||
if world.AreaRandomizer[player].value == 2: # Secret Regions as well
|
||||
secret_entrance_ids = list(range(len(sm64paintings), len(sm64paintings) + len(sm64secrets)))
|
||||
course_entrance_ids = list(range(len(sm64paintings)))
|
||||
if world.AreaRandomizer[player].value >= 1: # Some randomization is happening, randomize Courses
|
||||
world.random.shuffle(course_entrance_ids)
|
||||
if world.AreaRandomizer[player].value == 2: # Randomize Secrets as well
|
||||
world.random.shuffle(secret_entrance_ids)
|
||||
entrance_ids = course_entrance_ids + secret_entrance_ids
|
||||
if world.AreaRandomizer[player].value == 3: # Randomize Courses and Secrets in one pool
|
||||
world.random.shuffle(entrance_ids)
|
||||
# Guarantee first entrance is a course
|
||||
swaplist = list(range(len(entrance_ids)))
|
||||
@@ -117,7 +118,7 @@ def set_rules(world, player: int, area_connections):
|
||||
add_rule(world.get_location("Toad (Third Floor)", player), lambda state: state.can_reach("Third Floor", 'Region', player) and state.has("Power Star", player, 35))
|
||||
|
||||
if world.MIPS1Cost[player].value > world.MIPS2Cost[player].value:
|
||||
world.MIPS2Cost[player].value = world.MIPS1Cost[player].value
|
||||
(world.MIPS2Cost[player].value, world.MIPS1Cost[player].value) = (world.MIPS1Cost[player].value, world.MIPS2Cost[player].value)
|
||||
add_rule(world.get_location("MIPS 1", player), lambda state: state.can_reach("Basement", 'Region', player) and state.has("Power Star", player, world.MIPS1Cost[player].value))
|
||||
add_rule(world.get_location("MIPS 2", player), lambda state: state.can_reach("Basement", 'Region', player) and state.has("Power Star", player, world.MIPS2Cost[player].value))
|
||||
|
||||
|
||||
@@ -3,21 +3,17 @@ import asyncio
|
||||
import time
|
||||
|
||||
from NetUtils import ClientStatus, color
|
||||
from worlds import AutoWorldRegister
|
||||
from SNIClient import Context, snes_buffered_write, snes_flush_writes, snes_read
|
||||
from worlds.AutoSNIClient import SNIClient
|
||||
from .Names.TextBox import generate_received_text
|
||||
from Patch import GAME_SMW
|
||||
|
||||
snes_logger = logging.getLogger("SNES")
|
||||
|
||||
# FXPAK Pro protocol memory mapping used by SNI
|
||||
ROM_START = 0x000000
|
||||
WRAM_START = 0xF50000
|
||||
WRAM_SIZE = 0x20000
|
||||
SRAM_START = 0xE00000
|
||||
|
||||
SAVEDATA_START = WRAM_START + 0xF000
|
||||
SAVEDATA_SIZE = 0x500
|
||||
|
||||
SMW_ROMHASH_START = 0x7FC0
|
||||
ROMHASH_SIZE = 0x15
|
||||
|
||||
@@ -58,8 +54,12 @@ SMW_BAD_TEXT_BOX_LEVELS = [0x26, 0x02, 0x4B]
|
||||
SMW_BOSS_STATES = [0x80, 0xC0, 0xC1]
|
||||
SMW_UNCOLLECTABLE_LEVELS = [0x25, 0x07, 0x0B, 0x40, 0x0E, 0x1F, 0x20, 0x1B, 0x1A, 0x35, 0x34, 0x31, 0x32]
|
||||
|
||||
async def deathlink_kill_player(ctx: Context):
|
||||
if ctx.game == GAME_SMW:
|
||||
|
||||
class SMWSNIClient(SNIClient):
|
||||
game = "Super Mario World"
|
||||
|
||||
async def deathlink_kill_player(self, ctx):
|
||||
from SNIClient import DeathState, snes_buffered_write, snes_flush_writes, snes_read
|
||||
game_state = await snes_read(ctx, SMW_GAME_STATE_ADDR, 0x1)
|
||||
if game_state[0] != 0x14:
|
||||
return
|
||||
@@ -88,25 +88,19 @@ async def deathlink_kill_player(ctx: Context):
|
||||
|
||||
await snes_flush_writes(ctx)
|
||||
|
||||
from SNIClient import DeathState
|
||||
ctx.death_state = DeathState.dead
|
||||
ctx.last_death_link = time.time()
|
||||
|
||||
return
|
||||
|
||||
async def validate_rom(self, ctx):
|
||||
from SNIClient import snes_buffered_write, snes_flush_writes, snes_read
|
||||
|
||||
async def smw_rom_init(ctx: Context):
|
||||
if not ctx.rom:
|
||||
ctx.finished_game = False
|
||||
ctx.death_link_allow_survive = False
|
||||
game_hash = await snes_read(ctx, SMW_ROMHASH_START, ROMHASH_SIZE)
|
||||
if game_hash is None or game_hash == bytes([0] * ROMHASH_SIZE) or game_hash[:3] != b"SMW":
|
||||
rom_name = await snes_read(ctx, SMW_ROMHASH_START, ROMHASH_SIZE)
|
||||
if rom_name is None or rom_name == bytes([0] * ROMHASH_SIZE) or rom_name[:3] != b"SMW":
|
||||
return False
|
||||
else:
|
||||
ctx.game = GAME_SMW
|
||||
ctx.items_handling = 0b111 # remote items
|
||||
|
||||
ctx.rom = game_hash
|
||||
ctx.game = self.game
|
||||
ctx.items_handling = 0b111 # remote items
|
||||
|
||||
receive_option = await snes_read(ctx, SMW_RECEIVE_MSG_DATA, 0x1)
|
||||
send_option = await snes_read(ctx, SMW_SEND_MSG_DATA, 0x1)
|
||||
@@ -114,73 +108,73 @@ async def smw_rom_init(ctx: Context):
|
||||
ctx.receive_option = receive_option[0]
|
||||
ctx.send_option = send_option[0]
|
||||
|
||||
ctx.message_queue = []
|
||||
|
||||
ctx.allow_collect = True
|
||||
|
||||
death_link = await snes_read(ctx, SMW_DEATH_LINK_ACTIVE_ADDR, 1)
|
||||
if death_link:
|
||||
await ctx.update_death_link(bool(death_link[0] & 0b1))
|
||||
return True
|
||||
|
||||
ctx.rom = rom_name
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def add_message_to_queue(ctx: Context, new_message):
|
||||
def add_message_to_queue(self, new_message):
|
||||
|
||||
if not hasattr(ctx, "message_queue"):
|
||||
ctx.message_queue = []
|
||||
if not hasattr(self, "message_queue"):
|
||||
self.message_queue = []
|
||||
|
||||
ctx.message_queue.append(new_message)
|
||||
|
||||
return
|
||||
self.message_queue.append(new_message)
|
||||
|
||||
|
||||
async def handle_message_queue(ctx: Context):
|
||||
async def handle_message_queue(self, ctx):
|
||||
from SNIClient import snes_buffered_write, snes_flush_writes, snes_read
|
||||
|
||||
if not hasattr(self, "message_queue") or len(self.message_queue) == 0:
|
||||
return
|
||||
|
||||
game_state = await snes_read(ctx, SMW_GAME_STATE_ADDR, 0x1)
|
||||
if game_state[0] != 0x14:
|
||||
return
|
||||
|
||||
mario_state = await snes_read(ctx, SMW_MARIO_STATE_ADDR, 0x1)
|
||||
if mario_state[0] != 0x00:
|
||||
return
|
||||
|
||||
message_box = await snes_read(ctx, SMW_MESSAGE_BOX_ADDR, 0x1)
|
||||
if message_box[0] != 0x00:
|
||||
return
|
||||
|
||||
pause_state = await snes_read(ctx, SMW_PAUSE_ADDR, 0x1)
|
||||
if pause_state[0] != 0x00:
|
||||
return
|
||||
|
||||
current_level = await snes_read(ctx, SMW_CURRENT_LEVEL_ADDR, 0x1)
|
||||
if current_level[0] in SMW_BAD_TEXT_BOX_LEVELS:
|
||||
return
|
||||
|
||||
boss_state = await snes_read(ctx, SMW_BOSS_STATE_ADDR, 0x1)
|
||||
if boss_state[0] in SMW_BOSS_STATES:
|
||||
return
|
||||
|
||||
active_boss = await snes_read(ctx, SMW_ACTIVE_BOSS_ADDR, 0x1)
|
||||
if active_boss[0] != 0x00:
|
||||
return
|
||||
|
||||
next_message = self.message_queue.pop(0)
|
||||
|
||||
snes_buffered_write(ctx, SMW_MESSAGE_QUEUE_ADDR, bytes(next_message))
|
||||
snes_buffered_write(ctx, SMW_MESSAGE_BOX_ADDR, bytes([0x03]))
|
||||
snes_buffered_write(ctx, SMW_SFX_ADDR, bytes([0x22]))
|
||||
|
||||
await snes_flush_writes(ctx)
|
||||
|
||||
game_state = await snes_read(ctx, SMW_GAME_STATE_ADDR, 0x1)
|
||||
if game_state[0] != 0x14:
|
||||
return
|
||||
|
||||
mario_state = await snes_read(ctx, SMW_MARIO_STATE_ADDR, 0x1)
|
||||
if mario_state[0] != 0x00:
|
||||
return
|
||||
|
||||
message_box = await snes_read(ctx, SMW_MESSAGE_BOX_ADDR, 0x1)
|
||||
if message_box[0] != 0x00:
|
||||
return
|
||||
async def game_watcher(self, ctx):
|
||||
from SNIClient import snes_buffered_write, snes_flush_writes, snes_read
|
||||
|
||||
pause_state = await snes_read(ctx, SMW_PAUSE_ADDR, 0x1)
|
||||
if pause_state[0] != 0x00:
|
||||
return
|
||||
|
||||
current_level = await snes_read(ctx, SMW_CURRENT_LEVEL_ADDR, 0x1)
|
||||
if current_level[0] in SMW_BAD_TEXT_BOX_LEVELS:
|
||||
return
|
||||
|
||||
boss_state = await snes_read(ctx, SMW_BOSS_STATE_ADDR, 0x1)
|
||||
if boss_state[0] in SMW_BOSS_STATES:
|
||||
return
|
||||
|
||||
active_boss = await snes_read(ctx, SMW_ACTIVE_BOSS_ADDR, 0x1)
|
||||
if active_boss[0] != 0x00:
|
||||
return
|
||||
|
||||
if not hasattr(ctx, "message_queue") or len(ctx.message_queue) == 0:
|
||||
return
|
||||
|
||||
next_message = ctx.message_queue.pop(0)
|
||||
|
||||
snes_buffered_write(ctx, SMW_MESSAGE_QUEUE_ADDR, bytes(next_message))
|
||||
snes_buffered_write(ctx, SMW_MESSAGE_BOX_ADDR, bytes([0x03]))
|
||||
snes_buffered_write(ctx, SMW_SFX_ADDR, bytes([0x22]))
|
||||
|
||||
await snes_flush_writes(ctx)
|
||||
|
||||
return
|
||||
|
||||
|
||||
async def smw_game_watcher(ctx: Context):
|
||||
if ctx.game == GAME_SMW:
|
||||
# SMW_TODO: Handle Deathlink
|
||||
game_state = await snes_read(ctx, SMW_GAME_STATE_ADDR, 0x1)
|
||||
mario_state = await snes_read(ctx, SMW_MARIO_STATE_ADDR, 0x1)
|
||||
if game_state is None:
|
||||
@@ -234,7 +228,7 @@ async def smw_game_watcher(ctx: Context):
|
||||
snes_buffered_write(ctx, SMW_BONUS_STAR_ADDR, bytes([egg_count[0]]))
|
||||
await snes_flush_writes(ctx)
|
||||
|
||||
await handle_message_queue(ctx)
|
||||
await self.handle_message_queue(ctx)
|
||||
|
||||
new_checks = []
|
||||
event_data = await snes_read(ctx, SMW_EVENT_ROM_DATA, 0x60)
|
||||
@@ -243,6 +237,7 @@ async def smw_game_watcher(ctx: Context):
|
||||
dragon_coins_active = await snes_read(ctx, SMW_DRAGON_COINS_ACTIVE_ADDR, 0x1)
|
||||
from worlds.smw.Rom import item_rom_data, ability_rom_data
|
||||
from worlds.smw.Levels import location_id_to_level_id, level_info_dict
|
||||
from worlds import AutoWorldRegister
|
||||
for loc_name, level_data in location_id_to_level_id.items():
|
||||
loc_id = AutoWorldRegister.world_types[ctx.game].location_name_to_id[loc_name]
|
||||
if loc_id not in ctx.locations_checked:
|
||||
@@ -262,7 +257,6 @@ async def smw_game_watcher(ctx: Context):
|
||||
bit_set = (masked_data != 0)
|
||||
|
||||
if bit_set:
|
||||
# SMW_TODO: Handle non-included checks
|
||||
new_checks.append(loc_id)
|
||||
else:
|
||||
event_id_value = event_id + level_data[1]
|
||||
@@ -275,7 +269,6 @@ async def smw_game_watcher(ctx: Context):
|
||||
bit_set = (masked_data != 0)
|
||||
|
||||
if bit_set:
|
||||
# SMW_TODO: Handle non-included checks
|
||||
new_checks.append(loc_id)
|
||||
|
||||
verify_game_state = await snes_read(ctx, SMW_GAME_STATE_ADDR, 0x1)
|
||||
@@ -320,7 +313,7 @@ async def smw_game_watcher(ctx: Context):
|
||||
player_name = ctx.player_names[item.player]
|
||||
|
||||
receive_message = generate_received_text(item_name, player_name)
|
||||
add_message_to_queue(ctx, receive_message)
|
||||
self.add_message_to_queue(receive_message)
|
||||
|
||||
snes_buffered_write(ctx, SMW_RECV_PROGRESS_ADDR, bytes([recv_index]))
|
||||
if item.item in item_rom_data:
|
||||
@@ -372,7 +365,7 @@ async def smw_game_watcher(ctx: Context):
|
||||
rand_trap = random.choice(lit_trap_text_list)
|
||||
|
||||
for message in rand_trap:
|
||||
add_message_to_queue(ctx, message)
|
||||
self.add_message_to_queue(message)
|
||||
|
||||
await snes_flush_writes(ctx)
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ from .Levels import full_level_list, generate_level_list, location_id_to_level_i
|
||||
from .Rules import set_rules
|
||||
from ..generic.Rules import add_rule
|
||||
from .Names import ItemName, LocationName
|
||||
from .Client import SMWSNIClient
|
||||
from ..AutoWorld import WebWorld, World
|
||||
from .Rom import LocalRom, patch_rom, get_base_rom_path, SMWDeltaPatch
|
||||
|
||||
|
||||
118
worlds/smz3/Client.py
Normal file
118
worlds/smz3/Client.py
Normal file
@@ -0,0 +1,118 @@
|
||||
import logging
|
||||
import asyncio
|
||||
import time
|
||||
|
||||
from NetUtils import ClientStatus, color
|
||||
from worlds.AutoSNIClient import SNIClient
|
||||
from .Rom import ROM_PLAYER_LIMIT as SMZ3_ROM_PLAYER_LIMIT
|
||||
|
||||
snes_logger = logging.getLogger("SNES")
|
||||
|
||||
# FXPAK Pro protocol memory mapping used by SNI
|
||||
ROM_START = 0x000000
|
||||
WRAM_START = 0xF50000
|
||||
WRAM_SIZE = 0x20000
|
||||
SRAM_START = 0xE00000
|
||||
|
||||
# SMZ3
|
||||
SMZ3_ROMNAME_START = ROM_START + 0x00FFC0
|
||||
ROMNAME_SIZE = 0x15
|
||||
|
||||
SAVEDATA_START = WRAM_START + 0xF000
|
||||
|
||||
SMZ3_INGAME_MODES = {0x07, 0x09, 0x0B}
|
||||
ENDGAME_MODES = {0x19, 0x1A}
|
||||
SM_ENDGAME_MODES = {0x26, 0x27}
|
||||
SMZ3_DEATH_MODES = {0x15, 0x17, 0x18, 0x19, 0x1A}
|
||||
|
||||
SMZ3_RECV_PROGRESS_ADDR = SRAM_START + 0x4000 # 2 bytes
|
||||
SMZ3_RECV_ITEM_ADDR = SAVEDATA_START + 0x4D2 # 1 byte
|
||||
SMZ3_RECV_ITEM_PLAYER_ADDR = SAVEDATA_START + 0x4D3 # 1 byte
|
||||
|
||||
|
||||
class SMZ3SNIClient(SNIClient):
|
||||
game = "SMZ3"
|
||||
|
||||
async def validate_rom(self, ctx):
|
||||
from SNIClient import snes_buffered_write, snes_flush_writes, snes_read
|
||||
|
||||
rom_name = await snes_read(ctx, SMZ3_ROMNAME_START, ROMNAME_SIZE)
|
||||
if rom_name is None or rom_name == bytes([0] * ROMNAME_SIZE) or rom_name[:3] != b"ZSM":
|
||||
return False
|
||||
|
||||
ctx.game = self.game
|
||||
ctx.items_handling = 0b101 # local items and remote start inventory
|
||||
|
||||
ctx.rom = rom_name
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def game_watcher(self, ctx):
|
||||
from SNIClient import snes_buffered_write, snes_flush_writes, snes_read
|
||||
if ctx.server is None or ctx.slot is None:
|
||||
# not successfully connected to a multiworld server, cannot process the game sending items
|
||||
return
|
||||
|
||||
currentGame = await snes_read(ctx, SRAM_START + 0x33FE, 2)
|
||||
if (currentGame is not None):
|
||||
if (currentGame[0] != 0):
|
||||
gamemode = await snes_read(ctx, WRAM_START + 0x0998, 1)
|
||||
endGameModes = SM_ENDGAME_MODES
|
||||
else:
|
||||
gamemode = await snes_read(ctx, WRAM_START + 0x10, 1)
|
||||
endGameModes = ENDGAME_MODES
|
||||
|
||||
if gamemode is not None and (gamemode[0] in endGameModes):
|
||||
if not ctx.finished_game:
|
||||
await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}])
|
||||
ctx.finished_game = True
|
||||
return
|
||||
|
||||
data = await snes_read(ctx, SMZ3_RECV_PROGRESS_ADDR + 0x680, 4)
|
||||
if data is None:
|
||||
return
|
||||
|
||||
recv_index = data[0] | (data[1] << 8)
|
||||
recv_item = data[2] | (data[3] << 8)
|
||||
|
||||
while (recv_index < recv_item):
|
||||
item_address = recv_index * 8
|
||||
message = await snes_read(ctx, SMZ3_RECV_PROGRESS_ADDR + 0x700 + item_address, 8)
|
||||
is_z3_item = ((message[5] & 0x80) != 0)
|
||||
masked_part = (message[5] & 0x7F) if is_z3_item else message[5]
|
||||
item_index = ((message[4] | (masked_part << 8)) >> 3) + (256 if is_z3_item else 0)
|
||||
|
||||
recv_index += 1
|
||||
snes_buffered_write(ctx, SMZ3_RECV_PROGRESS_ADDR + 0x680, bytes([recv_index & 0xFF, (recv_index >> 8) & 0xFF]))
|
||||
|
||||
from worlds.smz3.TotalSMZ3.Location import locations_start_id
|
||||
from worlds.smz3 import convertLocSMZ3IDToAPID
|
||||
location_id = locations_start_id + convertLocSMZ3IDToAPID(item_index)
|
||||
|
||||
ctx.locations_checked.add(location_id)
|
||||
location = ctx.location_names[location_id]
|
||||
snes_logger.info(f'New Check: {location} ({len(ctx.locations_checked)}/{len(ctx.missing_locations) + len(ctx.checked_locations)})')
|
||||
await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": [location_id]}])
|
||||
|
||||
data = await snes_read(ctx, SMZ3_RECV_PROGRESS_ADDR + 0x600, 4)
|
||||
if data is None:
|
||||
return
|
||||
|
||||
item_out_ptr = data[2] | (data[3] << 8)
|
||||
|
||||
from worlds.smz3.TotalSMZ3.Item import items_start_id
|
||||
if item_out_ptr < len(ctx.items_received):
|
||||
item = ctx.items_received[item_out_ptr]
|
||||
item_id = item.item - items_start_id
|
||||
|
||||
player_id = item.player if item.player <= SMZ3_ROM_PLAYER_LIMIT else 0
|
||||
snes_buffered_write(ctx, SMZ3_RECV_PROGRESS_ADDR + item_out_ptr * 4, bytes([player_id & 0xFF, (player_id >> 8) & 0xFF, item_id & 0xFF, (item_id >> 8) & 0xFF]))
|
||||
item_out_ptr += 1
|
||||
snes_buffered_write(ctx, SMZ3_RECV_PROGRESS_ADDR + 0x602, bytes([item_out_ptr & 0xFF, (item_out_ptr >> 8) & 0xFF]))
|
||||
logging.info('Received %s from %s (%s) (%d/%d in list)' % (
|
||||
color(ctx.item_names[item.item], 'red', 'bold'), color(ctx.player_names[item.player], 'yellow'),
|
||||
ctx.location_names[item.location], item_out_ptr, len(ctx.items_received)))
|
||||
|
||||
await snes_flush_writes(ctx)
|
||||
|
||||
@@ -17,6 +17,7 @@ from worlds.smz3.TotalSMZ3.Location import LocationType, locations_start_id, Loc
|
||||
from worlds.smz3.TotalSMZ3.Patch import Patch as TotalSMZ3Patch, getWord, getWordArray
|
||||
from worlds.smz3.TotalSMZ3.WorldState import WorldState
|
||||
from ..AutoWorld import World, AutoLogicRegister, WebWorld
|
||||
from .Client import SMZ3SNIClient
|
||||
from .Rom import get_base_rom_bytes, SMZ3DeltaPatch
|
||||
from .ips import IPS_Patch
|
||||
from .Options import smz3_options
|
||||
|
||||
@@ -175,7 +175,7 @@ item_table: Dict[int, ItemDict] = {
|
||||
'name': 'Thermal Plant Fragment',
|
||||
'tech_type': 'ThermalPlantFragment'},
|
||||
35041: {'classification': ItemClassification.progression,
|
||||
'count': 2,
|
||||
'count': 4,
|
||||
'name': 'Seaglide Fragment',
|
||||
'tech_type': 'SeaglideFragment'},
|
||||
35042: {'classification': ItemClassification.progression,
|
||||
|
||||
@@ -44,14 +44,12 @@ class SubnauticaWorld(World):
|
||||
data_version = 7
|
||||
required_client_version = (0, 3, 5)
|
||||
|
||||
prefill_items: List[Item]
|
||||
creatures_to_scan: List[str]
|
||||
|
||||
def generate_early(self) -> None:
|
||||
self.prefill_items = [
|
||||
self.create_item("Seaglide Fragment"),
|
||||
self.create_item("Seaglide Fragment")
|
||||
]
|
||||
if "Seaglide Fragment" not in self.world.early_items[self.player]:
|
||||
self.world.early_items[self.player].value["Seaglide Fragment"] = 2
|
||||
|
||||
scan_option: Options.AggressiveScanLogic = self.world.creature_scan_logic[self.player]
|
||||
creature_pool = scan_option.get_pool()
|
||||
|
||||
@@ -149,16 +147,6 @@ class SubnauticaWorld(World):
|
||||
ret.exits.append(Entrance(self.player, region_exit, ret))
|
||||
return ret
|
||||
|
||||
def get_pre_fill_items(self) -> List[Item]:
|
||||
return self.prefill_items
|
||||
|
||||
def pre_fill(self) -> None:
|
||||
reachable = self.world.get_reachable_locations(player=self.player)
|
||||
self.world.random.shuffle(reachable)
|
||||
items = self.prefill_items.copy()
|
||||
for item in items:
|
||||
reachable.pop().place_locked_item(item)
|
||||
|
||||
|
||||
class SubnauticaLocation(Location):
|
||||
game: str = "Subnautica"
|
||||
|
||||
@@ -304,7 +304,8 @@ class ZillionWorld(World):
|
||||
zz_patcher.all_fixes_and_options(zz_options)
|
||||
zz_patcher.set_external_item_interface(zz_options.start_char, zz_options.max_level)
|
||||
zz_patcher.set_multiworld_items(multi_items)
|
||||
zz_patcher.set_rom_to_ram_data(self.world.player_name[self.player].replace(' ', '_').encode())
|
||||
game_id = self.world.player_name[self.player].encode() + b'\x00' + self.world.seed_name[-6:].encode()
|
||||
zz_patcher.set_rom_to_ram_data(game_id)
|
||||
|
||||
def generate_output(self, output_directory: str) -> None:
|
||||
"""This method gets called from a threadpool, do not use world.random here.
|
||||
|
||||
@@ -1 +1,4 @@
|
||||
import os
|
||||
|
||||
base_id = 8675309
|
||||
zillion_map = os.path.join(os.path.dirname(__file__), "empty-zillion-map-row-col-labels-281.png")
|
||||
|
||||
@@ -15,7 +15,9 @@
|
||||
RetroArch 1.9.x will not work, as it is older than 1.10.3.
|
||||
|
||||
1. Enter the RetroArch main menu screen.
|
||||
2. Go to Main Menu --> Online Updater --> Core Downloader. Scroll down and install "Sega - MS/GG (SMS Plus GX)".
|
||||
2. Go to Main Menu --> Online Updater --> Core Downloader. Scroll down and install one of these cores:
|
||||
- "Sega - MS/GG (SMS Plus GX)"
|
||||
- "Sega - MS/GG/MD/CD (Genesis Plus GX)
|
||||
3. Go to Settings --> User Interface. Set "Show Advanced Settings" to ON.
|
||||
4. Go to Settings --> Network. Set "Network Commands" to ON. (It is found below Request Device 16.) Leave the default
|
||||
Network Command Port at 55355.
|
||||
@@ -47,6 +49,15 @@ guide: [Basic Multiworld Setup Guide](/tutorial/Archipelago/setup/en)
|
||||
The [player settings page](/games/Zillion/player-settings) on the website allows you to configure your personal settings and export a config file from
|
||||
them.
|
||||
|
||||
### Advanced settings
|
||||
|
||||
The [advanced settings page](/tutorial/Archipelago/advanced_settings/en) describes more options you can put in your configuration file.
|
||||
- A recommended setting for Zillion is:
|
||||
```
|
||||
early_items:
|
||||
Scope: 1
|
||||
```
|
||||
|
||||
### Verifying your config file
|
||||
|
||||
If you would like to validate your config file to make sure it works, you may do so on the [YAML Validator page](/mysterycheck).
|
||||
@@ -63,7 +74,7 @@ If you would like to validate your config file to make sure it works, you may do
|
||||
- Windows
|
||||
- Double-click on your patch file.
|
||||
The Zillion Client will launch automatically, and create your ROM in the location of the patch file.
|
||||
6. Open the ROM in RetroArch using the core "SMS Plus GX".
|
||||
6. Open the ROM in RetroArch using the core "SMS Plus GX" or "Genesis Plus GX".
|
||||
- For a single player game, any emulator (or a Sega Master System) can be used, but there are additional features with RetroArch and the Zillion Client.
|
||||
- If you press reset or restore a save state and return to the surface in the game, the Zillion Client will keep open all the doors that you have opened.
|
||||
|
||||
@@ -80,7 +91,7 @@ If you would like to validate your config file to make sure it works, you may do
|
||||
- This should automatically launch the client, and will also create your ROM in the same place as your patch file.
|
||||
3. Connect to the client.
|
||||
- Use RetroArch to open the ROM that was generated.
|
||||
- Be sure to select the **SMS Plus GX** core. This core will allow external tools to read RAM data.
|
||||
- Be sure to select the **SMS Plus GX** core or the **Genesis Plus GX** core. These cores will allow external tools to read RAM data.
|
||||
4. Connect to the Archipelago Server.
|
||||
- The patch file which launched your client should have automatically connected you to the AP Server. There are a few reasons this may not happen however, including if the game is hosted on the website but was generated elsewhere. If the client window shows "Server Status: Not Connected", simply ask the host for the address of the server, and copy/paste it into the "Server" input field then press enter.
|
||||
- The client will attempt to reconnect to the new server address, and should momentarily show "Server Status: Connected".
|
||||
|
||||
BIN
worlds/zillion/empty-zillion-map-row-col-labels-281.png
Normal file
BIN
worlds/zillion/empty-zillion-map-row-col-labels-281.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 29 KiB |
@@ -26,11 +26,6 @@ class ZillionContinues(SpecialRange):
|
||||
}
|
||||
|
||||
|
||||
class ZillionEarlyScope(Toggle):
|
||||
""" whether to make sure there is a scope available early """
|
||||
display_name = "early scope"
|
||||
|
||||
|
||||
class ZillionFloppyReq(Range):
|
||||
""" how many floppy disks are required """
|
||||
range_start = 0
|
||||
@@ -227,7 +222,6 @@ class ZillionRoomGen(Toggle):
|
||||
|
||||
zillion_options: Dict[str, AssembleOptions] = {
|
||||
"continues": ZillionContinues,
|
||||
# "early_scope": ZillionEarlyScope, # TODO: implement
|
||||
"floppy_req": ZillionFloppyReq,
|
||||
"gun_levels": ZillionGunLevels,
|
||||
"jump_levels": ZillionJumpLevels,
|
||||
@@ -371,7 +365,7 @@ def validate(world: "MultiWorld", p: int) -> "Tuple[ZzOptions, Counter[str]]":
|
||||
floppy_req.value,
|
||||
wo.continues[p].value,
|
||||
wo.randomize_alarms[p].value,
|
||||
False, # wo.early_scope[p].value,
|
||||
False, # early scope can be done with AP early_items
|
||||
True, # balance defense
|
||||
starting_cards.value,
|
||||
bool(room_gen.value)
|
||||
|
||||
@@ -1 +1 @@
|
||||
git+https://github.com/beauxq/zilliandomizer@45a45eaca4119a4d06d2c31546ad19f3abd77f63#egg=zilliandomizer==0.4.4
|
||||
git+https://github.com/beauxq/zilliandomizer@c97298ecb1bca58c3dd3376a1e1609fad53788cf#egg=zilliandomizer==0.4.5
|
||||
|
||||
Reference in New Issue
Block a user