Compare commits

..

16 Commits

Author SHA1 Message Date
CaitSith2
2524ddc075 Dark Souls 3: Use options_dataclass 2023-12-24 17:30:53 -08:00
CaitSith2
d545b78803 Clique: Use options_dataclass 2023-12-24 17:30:17 -08:00
CaitSith2
88b1c94eb2 ChecksFinder: use options_dataclass 2023-12-24 17:29:30 -08:00
CaitSith2
7742d5d804 BumperStickers: Use options_dataclass 2023-12-24 17:29:04 -08:00
CaitSith2
d3e148dcc6 Blasphemous: Use options_dataclass 2023-12-24 17:28:34 -08:00
CaitSith2
b5fccde913 Didn't mean to include this debugging line. 2023-12-24 16:50:40 -08:00
CaitSith2
55e9b0687a Factorio: Options assigned as data_class 2023-12-24 16:49:05 -08:00
CaitSith2
79e1bf351e Adventure: options assigned as data_class 2023-12-24 16:47:54 -08:00
CaitSith2
fcfea9d9aa Adventure: use options.name instead of multiworld.name[players] 2023-12-24 15:22:07 -08:00
CaitSith2
cfc5508f06 ds3: use options.name instead of multiworld.name[player] 2023-12-24 14:10:33 -08:00
CaitSith2
62cb5f1fc2 fix flake8(push) hopefully 2023-12-24 13:43:19 -08:00
CaitSith2
7e70b16656 Clique: use options.name instead of multiworld.name 2023-12-24 13:34:25 -08:00
CaitSith2
7b486b3380 BumpStick: use options.name instead of multiworld.name 2023-12-24 13:33:32 -08:00
CaitSith2
09cac0a685 Core: use options.option_name instead of multiworld.option_name 2023-12-24 12:24:31 -08:00
CaitSith2
12c583533d Blasphemous: use options.option_name instead of multiworld.option_name 2023-12-24 11:43:28 -08:00
CaitSith2
c5af28a649 Factorio: use options.option_name instead of multiworld.option_name 2023-12-24 11:41:23 -08:00
48 changed files with 1382 additions and 1117 deletions

View File

@@ -167,10 +167,11 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
# remove starting inventory from pool items. # remove starting inventory from pool items.
# Because some worlds don't actually create items during create_items this has to be as late as possible. # Because some worlds don't actually create items during create_items this has to be as late as possible.
if any(world.start_inventory_from_pool[player].value for player in world.player_ids): if any(getattr(world.worlds[player].options, "start_inventory_from_pool", StartInventoryPool({})).value for player in world.player_ids):
new_items: List[Item] = [] new_items: List[Item] = []
depletion_pool: Dict[int, Dict[str, int]] = { depletion_pool: Dict[int, Dict[str, int]] = {
player: world.start_inventory_from_pool[player].value.copy() for player in world.player_ids} player: getattr(world.worlds[player].options, "start_inventory_from_pool", StartInventoryPool({})).value.copy()
for player in world.player_ids}
for player, items in depletion_pool.items(): for player, items in depletion_pool.items():
player_world: AutoWorld.World = world.worlds[player] player_world: AutoWorld.World = world.worlds[player]
for count in items.values(): for count in items.values():

View File

@@ -11,14 +11,11 @@ from flask import request, flash, redirect, url_for, session, render_template
from markupsafe import Markup from markupsafe import Markup
from pony.orm import commit, flush, select, rollback from pony.orm import commit, flush, select, rollback
from pony.orm.core import TransactionIntegrityError from pony.orm.core import TransactionIntegrityError
import schema
import MultiServer import MultiServer
from NetUtils import SlotType from NetUtils import SlotType
from Utils import VersionException, __version__ from Utils import VersionException, __version__
from worlds import GamesPackage
from worlds.Files import AutoPatchRegister from worlds.Files import AutoPatchRegister
from worlds.AutoWorld import data_package_checksum
from . import app from . import app
from .models import Seed, Room, Slot, GameDataPackage from .models import Seed, Room, Slot, GameDataPackage
@@ -26,15 +23,6 @@ banned_extensions = (".sfc", ".z64", ".n64", ".nes", ".smc", ".sms", ".gb", ".gb
allowed_options_extensions = (".yaml", ".json", ".yml", ".txt", ".zip") allowed_options_extensions = (".yaml", ".json", ".yml", ".txt", ".zip")
allowed_generation_extensions = (".archipelago", ".zip") allowed_generation_extensions = (".archipelago", ".zip")
games_package_schema = schema.Schema({
"item_name_groups": {str: [str]},
"item_name_to_id": {str: int},
"location_name_groups": {str: [str]},
"location_name_to_id": {str: int},
schema.Optional("checksum"): str,
schema.Optional("version"): int,
})
def allowed_options(filename: str) -> bool: def allowed_options(filename: str) -> bool:
return filename.endswith(allowed_options_extensions) return filename.endswith(allowed_options_extensions)
@@ -49,8 +37,6 @@ def banned_file(filename: str) -> bool:
def process_multidata(compressed_multidata, files={}): def process_multidata(compressed_multidata, files={}):
game_data: GamesPackage
decompressed_multidata = MultiServer.Context.decompress(compressed_multidata) decompressed_multidata = MultiServer.Context.decompress(compressed_multidata)
slots: typing.Set[Slot] = set() slots: typing.Set[Slot] = set()
@@ -59,19 +45,11 @@ def process_multidata(compressed_multidata, files={}):
game_data_packages: typing.List[GameDataPackage] = [] game_data_packages: typing.List[GameDataPackage] = []
for game, game_data in decompressed_multidata["datapackage"].items(): for game, game_data in decompressed_multidata["datapackage"].items():
if game_data.get("checksum"): if game_data.get("checksum"):
original_checksum = game_data.pop("checksum")
game_data = games_package_schema.validate(game_data)
game_data = {key: value for key, value in sorted(game_data.items())}
game_data["checksum"] = data_package_checksum(game_data)
game_data_package = GameDataPackage(checksum=game_data["checksum"], game_data_package = GameDataPackage(checksum=game_data["checksum"],
data=pickle.dumps(game_data)) data=pickle.dumps(game_data))
if original_checksum != game_data["checksum"]:
raise Exception(f"Original checksum {original_checksum} != "
f"calculated checksum {game_data['checksum']} "
f"for game {game}.")
decompressed_multidata["datapackage"][game] = { decompressed_multidata["datapackage"][game] = {
"version": game_data.get("version", 0), "version": game_data.get("version", 0),
"checksum": game_data["checksum"], "checksum": game_data["checksum"]
} }
try: try:
commit() # commit game data package commit() # commit game data package
@@ -86,15 +64,14 @@ def process_multidata(compressed_multidata, files={}):
if slot_info.type == SlotType.group: if slot_info.type == SlotType.group:
continue continue
slots.add(Slot(data=files.get(slot, None), slots.add(Slot(data=files.get(slot, None),
player_name=slot_info.name, player_name=slot_info.name,
player_id=slot, player_id=slot,
game=slot_info.game)) game=slot_info.game))
flush() # commit slots flush() # commit slots
compressed_multidata = compressed_multidata[0:1] + zlib.compress(pickle.dumps(decompressed_multidata), 9) compressed_multidata = compressed_multidata[0:1] + zlib.compress(pickle.dumps(decompressed_multidata), 9)
return slots, compressed_multidata return slots, compressed_multidata
def upload_zip_to_db(zfile: zipfile.ZipFile, owner=None, meta={"race": False}, sid=None): def upload_zip_to_db(zfile: zipfile.ZipFile, owner=None, meta={"race": False}, sid=None):
if not owner: if not owner:
owner = session["_id"] owner = session["_id"]

View File

@@ -1,10 +1,505 @@
import ModuleUpdate import asyncio
ModuleUpdate.update() import base64
import platform
from typing import Any, ClassVar, Coroutine, Dict, List, Optional, Protocol, Tuple, cast
import Utils # noqa: E402 # CommonClient import first to trigger ModuleUpdater
from CommonClient import CommonContext, server_loop, gui_enabled, \
ClientCommandProcessor, logger, get_base_parser
from NetUtils import ClientStatus
import Utils
from Utils import async_start
import colorama
from zilliandomizer.zri.memory import Memory
from zilliandomizer.zri import events
from zilliandomizer.utils.loc_name_maps import id_to_loc
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, zillion_map
class ZillionCommandProcessor(ClientCommandProcessor):
ctx: "ZillionContext"
def _cmd_sms(self) -> None:
""" Tell the client that Zillion is running in RetroArch. """
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"
command_processor = ZillionCommandProcessor
items_handling = 1 # receive items from other players
known_name: Optional[str]
""" This is almost the same as `auth` except `auth` is reset to `None` when server disconnects, and this isn't. """
from_game: "asyncio.Queue[events.EventFromGame]"
to_game: "asyncio.Queue[events.EventToGame]"
ap_local_count: int
""" local checks watched by server """
next_item: int
""" index in `items_received` """
ap_id_to_name: Dict[int, str]
ap_id_to_zz_id: Dict[int, int]
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 """
look_for_retroarch: asyncio.Event
"""
There is a bug in Python in Windows
https://github.com/python/cpython/issues/91227
that makes it so if I look for RetroArch before it's ready,
it breaks the asyncio udp transport system.
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.known_name = None
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":
# asyncio udp bug is only on Windows
self.look_for_retroarch.set()
self.reset_game_state()
def reset_game_state(self) -> None:
for _ in range(self.from_game.qsize()):
self.from_game.get_nowait()
for _ in range(self.to_game.qsize()):
self.to_game.get_nowait()
self.got_slot_data.clear()
self.ap_local_count = 0
self.next_item = 0
self.ap_id_to_name = {}
self.ap_id_to_zz_id = {}
self.rescues = {}
self.loc_mem_to_id = {}
self.locations_checked.clear()
self.missing_locations.clear()
self.checked_locations.clear()
self.finished_game = False
self.items_received.clear()
# override
def on_deathlink(self, data: Dict[str, Any]) -> None:
self.to_game.put_nowait(events.DeathEventToGame())
return super().on_deathlink(data)
# override
async def server_auth(self, password_requested: bool = False) -> None:
if password_requested and not self.password:
await super().server_auth(password_requested)
if not self.auth:
logger.info('waiting for connection to game...')
return
logger.info("logging in to server...")
await self.send_connect()
# 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 = [
("Client", "Archipelago")
]
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)
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:
logger.warn("`Connected` packet missing `slot_data`")
return
slot_data = args["slot_data"]
if "start_char" not in slot_data:
logger.warn("invalid Zillion `Connected` packet, `slot_data` missing `start_char`")
return
self.start_char = slot_data['start_char']
if self.start_char not in {"Apple", "Champ", "JJ"}:
logger.warn("invalid Zillion `Connected` packet, "
f"`slot_data` `start_char` has invalid value: {self.start_char}")
if "rescues" not in slot_data:
logger.warn("invalid Zillion `Connected` packet, `slot_data` missing `rescues`")
return
rescues = slot_data["rescues"]
self.rescues = {}
for rescue_id, json_info in rescues.items():
assert rescue_id in ("0", "1"), f"invalid rescue_id in Zillion slot_data: {rescue_id}"
# TODO: just take start_char out of the RescueInfo so there's no opportunity for a mismatch?
assert json_info["start_char"] == self.start_char, \
f'mismatch in Zillion slot data: {json_info["start_char"]} {self.start_char}'
ri = RescueInfo(json_info["start_char"],
json_info["room_code"],
json_info["mask"])
self.rescues[0 if rescue_id == "0" else 1] = ri
if "loc_mem_to_id" not in slot_data:
logger.warn("invalid Zillion `Connected` packet, `slot_data` missing `loc_mem_to_id`")
return
loc_mem_to_id = slot_data["loc_mem_to_id"]
self.loc_mem_to_id = {}
for mem_str, id_str in loc_mem_to_id.items():
mem = int(mem_str)
id_ = int(id_str)
room_i = mem // 256
assert 0 <= room_i < 74
assert id_ in id_to_loc
self.loc_mem_to_id[mem] = id_
if len(self.loc_mem_to_id) != 394:
logger.warn("invalid Zillion `Connected` packet, "
f"`slot_data` missing locations in `loc_mem_to_id` - len {len(self.loc_mem_to_id)}")
self.got_slot_data.set()
payload = {
"cmd": "Get",
"keys": [f"zillion-{self.auth}-doors"]
}
async_start(self.send_msgs([payload]))
elif cmd == "Retrieved":
if "keys" not in args:
logger.warning(f"invalid Retrieved packet to ZillionClient: {args}")
return
keys = cast(Dict[str, Optional[str]], args["keys"])
doors_b64 = keys.get(f"zillion-{self.auth}-doors", None)
if doors_b64:
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():
event_from_game = self.from_game.get_nowait()
if isinstance(event_from_game, events.AcquireLocationEventFromGame):
server_id = event_from_game.id + base_id
loc_name = id_to_loc[event_from_game.id]
self.locations_checked.add(server_id)
if server_id in self.missing_locations:
self.ap_local_count += 1
n_locations = len(self.missing_locations) + len(self.checked_locations) - 1 # -1 to ignore win
logger.info(f'New Check: {loc_name} ({self.ap_local_count}/{n_locations})')
async_start(self.send_msgs([
{"cmd": 'LocationChecks', "locations": [server_id]}
]))
else:
# This will happen a lot in Zillion,
# because all the key words are local and unwatched by the server.
logger.debug(f"DEBUG: {loc_name} not in missing")
elif isinstance(event_from_game, events.DeathEventFromGame):
async_start(self.send_death())
elif isinstance(event_from_game, events.WinEventFromGame):
if not self.finished_game:
async_start(self.send_msgs([
{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}
]))
self.finished_game = True
elif isinstance(event_from_game, events.DoorEventFromGame):
if self.auth:
doors_b64 = base64.b64encode(event_from_game.doors).decode()
payload = {
"cmd": "Set",
"key": f"zillion-{self.auth}-doors",
"operations": [{"operation": "replace", "value": doors_b64}]
}
async_start(self.send_msgs([payload]))
else:
logger.warning(f"WARNING: unhandled event from game {event_from_game}")
def process_items_received(self) -> None:
if len(self.items_received) > self.next_item:
zz_item_ids = [self.ap_id_to_zz_id[item.item] for item in self.items_received]
for index in range(self.next_item, len(self.items_received)):
ap_id = self.items_received[index].item
from_name = self.player_names[self.items_received[index].player]
# TODO: colors in this text, like sni client?
logger.info(f'received {self.ap_id_to_name[ap_id]} from {from_name}')
self.to_game.put_nowait(
events.ItemEventToGame(zz_item_ids)
)
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")
# to work around the Python bug where we can't check for RetroArch
if not ctx.look_for_retroarch.is_set():
logger.info("Start Zillion in RetroArch, then use the /sms command to connect to it.")
await asyncio.wait((
asyncio.create_task(ctx.look_for_retroarch.wait()),
asyncio.create_task(ctx.exit_event.wait())
), return_when=asyncio.FIRST_COMPLETED)
last_log = ""
def log_no_spam(msg: str) -> None:
nonlocal last_log
if msg != last_log:
last_log = msg
logger.info(msg)
# to only show this message once per client run
help_message_shown = False
with Memory(ctx.from_game, ctx.to_game) as memory:
while not ctx.exit_event.is_set():
ram = await memory.read()
game_id = memory.get_rom_to_ram_data(ram)
name, seed_end = name_seed_from_ram(game_id)
if len(name):
if name == ctx.known_name:
ctx.auth = name
# this is the name we know
if ctx.server and ctx.server.socket: # type: ignore
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
async_start(ctx.send_connect())
log_no_spam("logging in to server...")
await asyncio.wait((
asyncio.create_task(ctx.got_slot_data.wait()),
asyncio.create_task(ctx.exit_event.wait()),
asyncio.create_task(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
ctx.known_name = name
async_start(ctx.connect())
await asyncio.wait((
asyncio.create_task(ctx.got_room_info.wait()),
asyncio.create_task(ctx.exit_event.wait()),
asyncio.create_task(asyncio.sleep(6))
), 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.')
help_message_shown = True
log_no_spam("looking for connection to game...")
await asyncio.sleep(0.3)
await asyncio.sleep(0.09375)
logger.info("zillion sync task ending")
async def main() -> None:
parser = get_base_parser()
parser.add_argument('diff_file', default="", type=str, nargs="?",
help='Path to a .apzl Archipelago Binary Patch file')
# SNI parser.add_argument('--loglevel', default='info', choices=['debug', 'info', 'warning', 'error', 'critical'])
args = parser.parse_args()
print(args)
if args.diff_file:
import Patch
logger.info("patch file was supplied - creating sms rom...")
meta, rom_file = Patch.create_rom_file(args.diff_file)
if "server" in meta:
args.connect = meta["server"]
logger.info(f"wrote rom file to {rom_file}")
ctx = ZillionContext(args.connect, args.password)
if ctx.server_task is None:
ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop")
if gui_enabled:
ctx.run_gui()
ctx.run_cli()
sync_task = asyncio.create_task(zillion_sync_task(ctx))
await ctx.exit_event.wait()
ctx.server_address = None
logger.debug("waiting for sync task to end")
await sync_task
logger.debug("sync task ended")
await ctx.shutdown()
from worlds.zillion.client import launch # noqa: E402
if __name__ == "__main__": if __name__ == "__main__":
Utils.init_logging("ZillionClient", exception_logger="Client") Utils.init_logging("ZillionClient", exception_logger="Client")
launch()
colorama.init()
asyncio.run(main())
colorama.deinit()

View File

@@ -456,7 +456,6 @@ function send_receive ()
failed_guard_response = response failed_guard_response = response
end end
else else
if type(response) ~= "string" then response = "Unknown error" end
res[i] = {type = "ERROR", err = response} res[i] = {type = "ERROR", err = response}
end end
end end

View File

@@ -380,13 +380,12 @@ Additional arguments sent in this package will also be added to the [Retrieved](
Some special keys exist with specific return data, all of them have the prefix `_read_`, so `hints_{team}_{slot}` is `_read_hints_{team}_{slot}`. Some special keys exist with specific return data, all of them have the prefix `_read_`, so `hints_{team}_{slot}` is `_read_hints_{team}_{slot}`.
| Name | Type | Notes | | Name | Type | Notes |
|----------------------------------|-------------------------------|-------------------------------------------------------| |------------------------------|-------------------------------|---------------------------------------------------|
| hints_{team}_{slot} | list\[[Hint](#Hint)\] | All Hints belonging to the requested Player. | | hints_{team}_{slot} | list\[[Hint](#Hint)\] | All Hints belonging to the requested Player. |
| slot_data_{slot} | dict\[str, any\] | slot_data belonging to the requested slot. | | slot_data_{slot} | dict\[str, any\] | slot_data belonging to the requested slot. |
| item_name_groups_{game_name} | dict\[str, list\[str\]\] | item_name_groups belonging to the requested game. | | item_name_groups_{game_name} | dict\[str, list\[str\]\] | item_name_groups belonging to the requested game. |
| location_name_groups_{game_name} | dict\[str, list\[str\]\] | location_name_groups belonging to the requested game. | | client_status_{team}_{slot} | [ClientStatus](#ClientStatus) | The current game status of the requested player. |
| client_status_{team}_{slot} | [ClientStatus](#ClientStatus) | The current game status of the requested player. |
### Set ### Set
Used to write data to the server's data storage, that data can then be shared across worlds or just saved for later. Values for keys in the data storage can be retrieved with a [Get](#Get) package, or monitored with a [SetNotify](#SetNotify) package. Used to write data to the server's data storage, that data can then be shared across worlds or just saved for later. Values for keys in the data storage can be retrieved with a [Get](#Get) package, or monitored with a [SetNotify](#SetNotify) package.

View File

@@ -2,7 +2,9 @@ from __future__ import annotations
from typing import Dict from typing import Dict
from Options import Choice, Option, DefaultOnToggle, DeathLink, Range, Toggle from Options import Choice, Option, DefaultOnToggle, DeathLink, Range, Toggle, PerGameCommonOptions
from dataclasses import dataclass
class FreeincarnateMax(Range): class FreeincarnateMax(Range):
@@ -224,21 +226,20 @@ class StartCastle(Choice):
default = option_yellow default = option_yellow
adventure_option_definitions: Dict[str, type(Option)] = { @dataclass
"dragon_slay_check": DragonSlayCheck, class AdventureOptions(PerGameCommonOptions):
"death_link": DeathLink, dragon_slay_check: DragonSlayCheck
"bat_logic": BatLogic, death_link: DeathLink
"freeincarnate_max": FreeincarnateMax, bat_logic: BatLogic
"dragon_rando_type": DragonRandoType, freeincarnate_max: FreeincarnateMax
"connector_multi_slot": ConnectorMultiSlot, dragon_rando_type: DragonRandoType
"yorgle_speed": YorgleStartingSpeed, connector_multi_slot: ConnectorMultiSlot
"yorgle_min_speed": YorgleMinimumSpeed, yorgle_speed: YorgleStartingSpeed
"grundle_speed": GrundleStartingSpeed, yorgle_min_speed: YorgleMinimumSpeed
"grundle_min_speed": GrundleMinimumSpeed, grundle_speed: GrundleStartingSpeed
"rhindle_speed": RhindleStartingSpeed, grundle_min_speed: GrundleMinimumSpeed
"rhindle_min_speed": RhindleMinimumSpeed, rhindle_speed: RhindleStartingSpeed
"difficulty_switch_a": DifficultySwitchA, rhindle_min_speed: RhindleMinimumSpeed
"difficulty_switch_b": DifficultySwitchB, difficulty_switch_a: DifficultySwitchA
"start_castle": StartCastle, difficulty_switch_b: DifficultySwitchB
start_castle: StartCastle
}

View File

@@ -1,5 +1,6 @@
from BaseClasses import MultiWorld, Region, Entrance, LocationProgressType from BaseClasses import MultiWorld, Region, Entrance, LocationProgressType
from .Locations import location_table, LocationData, AdventureLocation, dragon_room_to_region from .Locations import location_table, LocationData, AdventureLocation, dragon_room_to_region
from Options import PerGameCommonOptions
def connect(world: MultiWorld, player: int, source: str, target: str, rule: callable = lambda state: True, def connect(world: MultiWorld, player: int, source: str, target: str, rule: callable = lambda state: True,
@@ -24,7 +25,7 @@ def connect(world: MultiWorld, player: int, source: str, target: str, rule: call
connect(world, player, target, source, rule, True) connect(world, player, target, source, rule, True)
def create_regions(multiworld: MultiWorld, player: int, dragon_rooms: []) -> None: def create_regions(options: PerGameCommonOptions, multiworld: MultiWorld, player: int, dragon_rooms: []) -> None:
for name, locdata in location_table.items(): for name, locdata in location_table.items():
locdata.get_position(multiworld.random) locdata.get_position(multiworld.random)
@@ -76,7 +77,7 @@ def create_regions(multiworld: MultiWorld, player: int, dragon_rooms: []) -> Non
credits_room_far_side.exits.append(Entrance(player, "CreditsFromFarSide", credits_room_far_side)) credits_room_far_side.exits.append(Entrance(player, "CreditsFromFarSide", credits_room_far_side))
multiworld.regions.append(credits_room_far_side) multiworld.regions.append(credits_room_far_side)
dragon_slay_check = multiworld.dragon_slay_check[player].value dragon_slay_check = options.dragon_slay_check.value
priority_locations = determine_priority_locations(multiworld, dragon_slay_check) priority_locations = determine_priority_locations(multiworld, dragon_slay_check)
for name, location_data in location_table.items(): for name, location_data in location_table.items():

View File

@@ -6,7 +6,8 @@ from BaseClasses import LocationProgressType
def set_rules(self) -> None: def set_rules(self) -> None:
world = self.multiworld world = self.multiworld
use_bat_logic = world.bat_logic[self.player].value == BatLogic.option_use_logic options = self.options
use_bat_logic = options.bat_logic.value == BatLogic.option_use_logic
set_rule(world.get_entrance("YellowCastlePort", self.player), set_rule(world.get_entrance("YellowCastlePort", self.player),
lambda state: state.has("Yellow Key", self.player)) lambda state: state.has("Yellow Key", self.player))
@@ -28,7 +29,7 @@ def set_rules(self) -> None:
lambda state: state.has("Bridge", self.player) or lambda state: state.has("Bridge", self.player) or
state.has("Magnet", self.player)) state.has("Magnet", self.player))
dragon_slay_check = world.dragon_slay_check[self.player].value dragon_slay_check = options.dragon_slay_check.value
if dragon_slay_check: if dragon_slay_check:
if self.difficulty_switch_b == DifficultySwitchB.option_hard_with_unlock_item: if self.difficulty_switch_b == DifficultySwitchB.option_hard_with_unlock_item:
set_rule(world.get_location("Slay Yorgle", self.player), set_rule(world.get_location("Slay Yorgle", self.player),

View File

@@ -15,7 +15,7 @@ from Options import AssembleOptions
from worlds.AutoWorld import WebWorld, World from worlds.AutoWorld import WebWorld, World
from Fill import fill_restrictive from Fill import fill_restrictive
from worlds.generic.Rules import add_rule, set_rule from worlds.generic.Rules import add_rule, set_rule
from .Options import adventure_option_definitions, DragonRandoType, DifficultySwitchA, DifficultySwitchB from .Options import AdventureOptions, DragonRandoType, DifficultySwitchA, DifficultySwitchB
from .Rom import get_base_rom_bytes, get_base_rom_path, AdventureDeltaPatch, apply_basepatch, \ from .Rom import get_base_rom_bytes, get_base_rom_path, AdventureDeltaPatch, apply_basepatch, \
AdventureAutoCollectLocation AdventureAutoCollectLocation
from .Items import item_table, ItemData, nothing_item_id, event_table, AdventureItem, standard_item_max from .Items import item_table, ItemData, nothing_item_id, event_table, AdventureItem, standard_item_max
@@ -109,7 +109,8 @@ class AdventureWorld(World):
game: ClassVar[str] = "Adventure" game: ClassVar[str] = "Adventure"
web: ClassVar[WebWorld] = AdventureWeb() web: ClassVar[WebWorld] = AdventureWeb()
option_definitions: ClassVar[Dict[str, AssembleOptions]] = adventure_option_definitions options = AdventureOptions
options_dataclass = AdventureOptions
settings: ClassVar[AdventureSettings] settings: ClassVar[AdventureSettings]
item_name_to_id: ClassVar[Dict[str, int]] = {name: data.id for name, data in item_table.items()} item_name_to_id: ClassVar[Dict[str, int]] = {name: data.id for name, data in item_table.items()}
location_name_to_id: ClassVar[Dict[str, int]] = {name: data.location_id for name, data in location_table.items()} location_name_to_id: ClassVar[Dict[str, int]] = {name: data.location_id for name, data in location_table.items()}
@@ -150,18 +151,18 @@ class AdventureWorld(World):
bytearray(f"ADVENTURE{__version__.replace('.', '')[:3]}_{self.player}_{self.multiworld.seed}", "utf8")[:21] bytearray(f"ADVENTURE{__version__.replace('.', '')[:3]}_{self.player}_{self.multiworld.seed}", "utf8")[:21]
self.rom_name.extend([0] * (21 - len(self.rom_name))) self.rom_name.extend([0] * (21 - len(self.rom_name)))
self.dragon_rando_type = self.multiworld.dragon_rando_type[self.player].value self.dragon_rando_type = self.options.dragon_rando_type.value
self.dragon_slay_check = self.multiworld.dragon_slay_check[self.player].value self.dragon_slay_check = self.options.dragon_slay_check.value
self.connector_multi_slot = self.multiworld.connector_multi_slot[self.player].value self.connector_multi_slot = self.options.connector_multi_slot.value
self.yorgle_speed = self.multiworld.yorgle_speed[self.player].value self.yorgle_speed = self.options.yorgle_speed.value
self.yorgle_min_speed = self.multiworld.yorgle_min_speed[self.player].value self.yorgle_min_speed = self.options.yorgle_min_speed.value
self.grundle_speed = self.multiworld.grundle_speed[self.player].value self.grundle_speed = self.options.grundle_speed.value
self.grundle_min_speed = self.multiworld.grundle_min_speed[self.player].value self.grundle_min_speed = self.options.grundle_min_speed.value
self.rhindle_speed = self.multiworld.rhindle_speed[self.player].value self.rhindle_speed = self.options.rhindle_speed.value
self.rhindle_min_speed = self.multiworld.rhindle_min_speed[self.player].value self.rhindle_min_speed = self.options.rhindle_min_speed.value
self.difficulty_switch_a = self.multiworld.difficulty_switch_a[self.player].value self.difficulty_switch_a = self.options.difficulty_switch_a.value
self.difficulty_switch_b = self.multiworld.difficulty_switch_b[self.player].value self.difficulty_switch_b = self.options.difficulty_switch_b.value
self.start_castle = self.multiworld.start_castle[self.player].value self.start_castle = self.options.start_castle.value
self.created_items = 0 self.created_items = 0
if self.dragon_slay_check == 0: if self.dragon_slay_check == 0:
@@ -228,7 +229,7 @@ class AdventureWorld(World):
extra_filler_count = num_locations - self.created_items extra_filler_count = num_locations - self.created_items
# traps would probably go here, if enabled # traps would probably go here, if enabled
freeincarnate_max = self.multiworld.freeincarnate_max[self.player].value freeincarnate_max = self.options.freeincarnate_max.value
actual_freeincarnates = min(extra_filler_count, freeincarnate_max) actual_freeincarnates = min(extra_filler_count, freeincarnate_max)
self.multiworld.itempool += [self.create_item("Freeincarnate") for _ in range(actual_freeincarnates)] self.multiworld.itempool += [self.create_item("Freeincarnate") for _ in range(actual_freeincarnates)]
self.created_items += actual_freeincarnates self.created_items += actual_freeincarnates
@@ -248,7 +249,7 @@ class AdventureWorld(World):
self.created_items += 1 self.created_items += 1
def create_regions(self) -> None: def create_regions(self) -> None:
create_regions(self.multiworld, self.player, self.dragon_rooms) create_regions(self.options, self.multiworld, self.player, self.dragon_rooms)
set_rules = set_rules set_rules = set_rules
@@ -355,7 +356,7 @@ class AdventureWorld(World):
auto_collect_locations: [AdventureAutoCollectLocation] = [] auto_collect_locations: [AdventureAutoCollectLocation] = []
local_item_to_location: {int, int} = {} local_item_to_location: {int, int} = {}
bat_no_touch_locs: [LocationData] = [] bat_no_touch_locs: [LocationData] = []
bat_logic: int = self.multiworld.bat_logic[self.player].value bat_logic: int = self.options.bat_logic.value
try: try:
rom_deltas: { int, int } = {} rom_deltas: { int, int } = {}
self.place_dragons(rom_deltas) self.place_dragons(rom_deltas)
@@ -411,7 +412,7 @@ class AdventureWorld(World):
item_position_data_start = get_item_position_data_start(unplaced_item.table_index) item_position_data_start = get_item_position_data_start(unplaced_item.table_index)
rom_deltas[item_position_data_start] = 0xff rom_deltas[item_position_data_start] = 0xff
if self.multiworld.connector_multi_slot[self.player].value: if self.options.connector_multi_slot.value:
rom_deltas[connector_port_offset] = (self.player & 0xff) rom_deltas[connector_port_offset] = (self.player & 0xff)
else: else:
rom_deltas[connector_port_offset] = 0 rom_deltas[connector_port_offset] = 0

View File

@@ -1,4 +1,5 @@
from Options import Choice, Toggle, DefaultOnToggle, DeathLink, StartInventoryPool from Options import Choice, Toggle, DefaultOnToggle, DeathLink, StartInventoryPool, PerGameCommonOptions
from dataclasses import dataclass
import random import random
@@ -163,26 +164,26 @@ class BlasphemousDeathLink(DeathLink):
Note that Guilt Fragments will not appear when killed by Death Link.""" Note that Guilt Fragments will not appear when killed by Death Link."""
blasphemous_options = { @dataclass
"prie_dieu_warp": PrieDieuWarp, class BlasphemousOptions(PerGameCommonOptions):
"skip_cutscenes": SkipCutscenes, prie_dieu_warp: PrieDieuWarp
"corpse_hints": CorpseHints, skip_cutscenes: SkipCutscenes
"difficulty": Difficulty, corpse_hints: CorpseHints
"penitence": Penitence, difficulty: Difficulty
"starting_location": StartingLocation, penitence: Penitence
"ending": Ending, starting_location: StartingLocation
"skip_long_quests": SkipLongQuests, ending: Ending
"thorn_shuffle" : ThornShuffle, skip_long_quests: SkipLongQuests
"dash_shuffle": DashShuffle, thorn_shuffle : ThornShuffle
"wall_climb_shuffle": WallClimbShuffle, dash_shuffle: DashShuffle
"reliquary_shuffle": ReliquaryShuffle, wall_climb_shuffle: WallClimbShuffle
"boots_of_pleading": CustomItem1, reliquary_shuffle: ReliquaryShuffle
"purified_hand": CustomItem2, boots_of_pleading: CustomItem1
"start_wheel": StartWheel, purified_hand: CustomItem2
"skill_randomizer": SkillRando, start_wheel: StartWheel
"enemy_randomizer": EnemyRando, skill_randomizer: SkillRando
"enemy_groups": EnemyGroups, enemy_randomizer: EnemyRando
"enemy_scaling": EnemyScaling, enemy_groups: EnemyGroups
"death_link": BlasphemousDeathLink, enemy_scaling: EnemyScaling
"start_inventory": StartInventoryPool death_link: BlasphemousDeathLink
} start_inventory: StartInventoryPool

View File

@@ -497,8 +497,9 @@ def chalice_rooms(state: CollectionState, player: int, number: int) -> bool:
def rules(blasphemousworld): def rules(blasphemousworld):
world = blasphemousworld.multiworld world = blasphemousworld.multiworld
player = blasphemousworld.player player = blasphemousworld.player
logic = world.difficulty[player].value options = blasphemousworld.options
enemy = world.enemy_randomizer[player].value logic = options.difficulty.value
enemy = options.enemy_randomizer.value
# D01Z01S01 (The Holy Line) # D01Z01S01 (The Holy Line)
@@ -2488,7 +2489,7 @@ def rules(blasphemousworld):
# D04Z02S01 (Mother of Mothers) # D04Z02S01 (Mother of Mothers)
# Items # Items
if world.purified_hand[player]: if options.purified_hand:
set_rule(world.get_location("MoM: Western room ledge", player), set_rule(world.get_location("MoM: Western room ledge", player),
lambda state: ( lambda state: (
state.has("D04Z02S01[N]", player) state.has("D04Z02S01[N]", player)
@@ -4093,7 +4094,7 @@ def rules(blasphemousworld):
# D17Z01S04 (Brotherhood of the Silent Sorrow) # D17Z01S04 (Brotherhood of the Silent Sorrow)
# Items # Items
if world.boots_of_pleading[player]: if options.boots_of_pleading:
set_rule(world.get_location("BotSS: 2nd meeting with Redento", player), set_rule(world.get_location("BotSS: 2nd meeting with Redento", player),
lambda state: redento(state, blasphemousworld, player, 2)) lambda state: redento(state, blasphemousworld, player, 2))
# Doors # Doors

View File

@@ -7,7 +7,7 @@ from .Locations import location_table
from .Rooms import room_table, door_table from .Rooms import room_table, door_table
from .Rules import rules from .Rules import rules
from worlds.generic.Rules import set_rule, add_rule from worlds.generic.Rules import set_rule, add_rule
from .Options import blasphemous_options from .Options import BlasphemousOptions
from .Vanilla import unrandomized_dict, junk_locations, thorn_set, skill_dict from .Vanilla import unrandomized_dict, junk_locations, thorn_set, skill_dict
@@ -39,7 +39,8 @@ class BlasphemousWorld(World):
location_name_to_game_id = {loc["name"]: loc["game_id"] for loc in location_table} location_name_to_game_id = {loc["name"]: loc["game_id"] for loc in location_table}
item_name_groups = group_table item_name_groups = group_table
option_definitions = blasphemous_options options = BlasphemousOptions
options_dataclass = BlasphemousOptions
required_client_version = (0, 4, 2) required_client_version = (0, 4, 2)
@@ -73,60 +74,61 @@ class BlasphemousWorld(World):
def generate_early(self): def generate_early(self):
options = self.options
world = self.multiworld world = self.multiworld
player = self.player player = self.player
if not world.starting_location[player].randomized: if not options.starting_location.randomized:
if world.starting_location[player].value == 6 and world.difficulty[player].value < 2: if options.starting_location.value == 6 and options.difficulty.value < 2:
raise Exception(f"[Blasphemous - '{world.get_player_name(player)}'] {world.starting_location[player]}" raise Exception(f"[Blasphemous - '{world.get_player_name(player)}'] {options.starting_location}"
" cannot be chosen if Difficulty is lower than Hard.") " cannot be chosen if Difficulty is lower than Hard.")
if (world.starting_location[player].value == 0 or world.starting_location[player].value == 6) \ if (options.starting_location.value == 0 or options.starting_location.value == 6) \
and world.dash_shuffle[player]: and options.dash_shuffle:
raise Exception(f"[Blasphemous - '{world.get_player_name(player)}'] {world.starting_location[player]}" raise Exception(f"[Blasphemous - '{world.get_player_name(player)}'] {options.starting_location}"
" cannot be chosen if Shuffle Dash is enabled.") " cannot be chosen if Shuffle Dash is enabled.")
if world.starting_location[player].value == 3 and world.wall_climb_shuffle[player]: if options.starting_location.value == 3 and options.wall_climb_shuffle:
raise Exception(f"[Blasphemous - '{world.get_player_name(player)}'] {world.starting_location[player]}" raise Exception(f"[Blasphemous - '{world.get_player_name(player)}'] {options.starting_location}"
" cannot be chosen if Shuffle Wall Climb is enabled.") " cannot be chosen if Shuffle Wall Climb is enabled.")
else: else:
locations: List[int] = [ 0, 1, 2, 3, 4, 5, 6 ] locations: List[int] = [ 0, 1, 2, 3, 4, 5, 6 ]
invalid: bool = False invalid: bool = False
if world.difficulty[player].value < 2: if options.difficulty.value < 2:
locations.remove(6) locations.remove(6)
if world.dash_shuffle[player]: if options.dash_shuffle:
locations.remove(0) locations.remove(0)
if 6 in locations: if 6 in locations:
locations.remove(6) locations.remove(6)
if world.wall_climb_shuffle[player]: if options.wall_climb_shuffle:
locations.remove(3) locations.remove(3)
if world.starting_location[player].value == 6 and world.difficulty[player].value < 2: if options.starting_location.value == 6 and options.difficulty.value < 2:
invalid = True invalid = True
if (world.starting_location[player].value == 0 or world.starting_location[player].value == 6) \ if (options.starting_location.value == 0 or options.starting_location.value == 6) \
and world.dash_shuffle[player]: and options.dash_shuffle:
invalid = True invalid = True
if world.starting_location[player].value == 3 and world.wall_climb_shuffle[player]: if options.starting_location.value == 3 and options.wall_climb_shuffle:
invalid = True invalid = True
if invalid: if invalid:
world.starting_location[player].value = world.random.choice(locations) options.starting_location.value = world.random.choice(locations)
if not world.dash_shuffle[player]: if not options.dash_shuffle:
world.push_precollected(self.create_item("Dash Ability")) world.push_precollected(self.create_item("Dash Ability"))
if not world.wall_climb_shuffle[player]: if not options.wall_climb_shuffle:
world.push_precollected(self.create_item("Wall Climb Ability")) world.push_precollected(self.create_item("Wall Climb Ability"))
if world.skip_long_quests[player]: if options.skip_long_quests:
for loc in junk_locations: for loc in junk_locations:
world.exclude_locations[player].value.add(loc) options.exclude_locations.value.add(loc)
start_rooms: Dict[int, str] = { start_rooms: Dict[int, str] = {
0: "D17Z01S01", 0: "D17Z01S01",
@@ -138,12 +140,12 @@ class BlasphemousWorld(World):
6: "D20Z02S09" 6: "D20Z02S09"
} }
self.start_room = start_rooms[world.starting_location[player].value] self.start_room = start_rooms[options.starting_location.value]
def create_items(self): def create_items(self):
options = self.options
world = self.multiworld world = self.multiworld
player = self.player
removed: int = 0 removed: int = 0
to_remove: List[str] = [ to_remove: List[str] = [
@@ -157,46 +159,46 @@ class BlasphemousWorld(World):
skipped_items = [] skipped_items = []
junk: int = 0 junk: int = 0
for item, count in world.start_inventory[player].value.items(): for item, count in options.start_inventory.value.items():
for _ in range(count): for _ in range(count):
skipped_items.append(item) skipped_items.append(item)
junk += 1 junk += 1
skipped_items.extend(unrandomized_dict.values()) skipped_items.extend(unrandomized_dict.values())
if world.thorn_shuffle[player] == 2: if options.thorn_shuffle == 2:
for i in range(8): for i in range(8):
skipped_items.append("Thorn Upgrade") skipped_items.append("Thorn Upgrade")
if world.dash_shuffle[player]: if options.dash_shuffle:
skipped_items.append(to_remove[removed]) skipped_items.append(to_remove[removed])
removed += 1 removed += 1
elif not world.dash_shuffle[player]: elif not options.dash_shuffle:
skipped_items.append("Dash Ability") skipped_items.append("Dash Ability")
if world.wall_climb_shuffle[player]: if options.wall_climb_shuffle:
skipped_items.append(to_remove[removed]) skipped_items.append(to_remove[removed])
removed += 1 removed += 1
elif not world.wall_climb_shuffle[player]: elif not options.wall_climb_shuffle:
skipped_items.append("Wall Climb Ability") skipped_items.append("Wall Climb Ability")
if not world.reliquary_shuffle[player]: if not options.reliquary_shuffle:
skipped_items.extend(reliquary_set) skipped_items.extend(reliquary_set)
elif world.reliquary_shuffle[player]: elif options.reliquary_shuffle:
for i in range(3): for i in range(3):
skipped_items.append(to_remove[removed]) skipped_items.append(to_remove[removed])
removed += 1 removed += 1
if not world.boots_of_pleading[player]: if not options.boots_of_pleading:
skipped_items.append("Boots of Pleading") skipped_items.append("Boots of Pleading")
if not world.purified_hand[player]: if not options.purified_hand:
skipped_items.append("Purified Hand of the Nun") skipped_items.append("Purified Hand of the Nun")
if world.start_wheel[player]: if options.start_wheel:
skipped_items.append("The Young Mason's Wheel") skipped_items.append("The Young Mason's Wheel")
if not world.skill_randomizer[player]: if not options.skill_randomizer:
skipped_items.extend(skill_dict.values()) skipped_items.extend(skill_dict.values())
counter = Counter(skipped_items) counter = Counter(skipped_items)
@@ -219,23 +221,24 @@ class BlasphemousWorld(World):
def pre_fill(self): def pre_fill(self):
options = self.options
world = self.multiworld world = self.multiworld
player = self.player player = self.player
self.place_items_from_dict(unrandomized_dict) self.place_items_from_dict(unrandomized_dict)
if world.thorn_shuffle[player] == 2: if options.thorn_shuffle == 2:
self.place_items_from_set(thorn_set, "Thorn Upgrade") self.place_items_from_set(thorn_set, "Thorn Upgrade")
if world.start_wheel[player]: if options.start_wheel:
world.get_location("Beginning gift", player)\ world.get_location("Beginning gift", player)\
.place_locked_item(self.create_item("The Young Mason's Wheel")) .place_locked_item(self.create_item("The Young Mason's Wheel"))
if not world.skill_randomizer[player]: if not options.skill_randomizer:
self.place_items_from_dict(skill_dict) self.place_items_from_dict(skill_dict)
if world.thorn_shuffle[player] == 1: if options.thorn_shuffle == 1:
world.local_items[player].value.add("Thorn Upgrade") options.local_items.value.add("Thorn Upgrade")
def place_items_from_set(self, location_set: Set[str], name: str): def place_items_from_set(self, location_set: Set[str], name: str):
@@ -251,6 +254,7 @@ class BlasphemousWorld(World):
def create_regions(self) -> None: def create_regions(self) -> None:
options = self.options
player = self.player player = self.player
world = self.multiworld world = self.multiworld
@@ -282,9 +286,9 @@ class BlasphemousWorld(World):
}) })
for index, loc in enumerate(location_table): for index, loc in enumerate(location_table):
if not world.boots_of_pleading[player] and loc["name"] == "BotSS: 2nd meeting with Redento": if not options.boots_of_pleading and loc["name"] == "BotSS: 2nd meeting with Redento":
continue continue
if not world.purified_hand[player] and loc["name"] == "MoM: Western room ledge": if not options.purified_hand and loc["name"] == "MoM: Western room ledge":
continue continue
region: Region = world.get_region(loc["room"], player) region: Region = world.get_region(loc["room"], player)
@@ -310,9 +314,9 @@ class BlasphemousWorld(World):
victory.place_locked_item(self.create_event("Victory")) victory.place_locked_item(self.create_event("Victory"))
world.get_region("D07Z01S03", player).locations.append(victory) world.get_region("D07Z01S03", player).locations.append(victory)
if world.ending[self.player].value == 1: if options.ending.value == 1:
set_rule(victory, lambda state: state.has("Thorn Upgrade", player, 8)) set_rule(victory, lambda state: state.has("Thorn Upgrade", player, 8))
elif world.ending[self.player].value == 2: elif options.ending.value == 2:
set_rule(victory, lambda state: state.has("Thorn Upgrade", player, 8) and set_rule(victory, lambda state: state.has("Thorn Upgrade", player, 8) and
state.has("Holy Wound of Abnegation", player)) state.has("Holy Wound of Abnegation", player))
@@ -332,11 +336,12 @@ class BlasphemousWorld(World):
locations = [] locations = []
doors: Dict[str, str] = {} doors: Dict[str, str] = {}
options = self.options
world = self.multiworld world = self.multiworld
player = self.player player = self.player
thorns: bool = True thorns: bool = True
if world.thorn_shuffle[player].value == 2: if options.thorn_shuffle.value == 2:
thorns = False thorns = False
for loc in world.get_filled_locations(player): for loc in world.get_filled_locations(player):
@@ -354,28 +359,28 @@ class BlasphemousWorld(World):
locations.append(data) locations.append(data)
config = { config = {
"LogicDifficulty": world.difficulty[player].value, "LogicDifficulty": options.difficulty.value,
"StartingLocation": world.starting_location[player].value, "StartingLocation": options.starting_location.value,
"VersionCreated": "AP", "VersionCreated": "AP",
"UnlockTeleportation": bool(world.prie_dieu_warp[player].value), "UnlockTeleportation": bool(options.prie_dieu_warp.value),
"AllowHints": bool(world.corpse_hints[player].value), "AllowHints": bool(options.corpse_hints.value),
"AllowPenitence": bool(world.penitence[player].value), "AllowPenitence": bool(options.penitence.value),
"ShuffleReliquaries": bool(world.reliquary_shuffle[player].value), "ShuffleReliquaries": bool(options.reliquary_shuffle.value),
"ShuffleBootsOfPleading": bool(world.boots_of_pleading[player].value), "ShuffleBootsOfPleading": bool(options.boots_of_pleading.value),
"ShufflePurifiedHand": bool(world.purified_hand[player].value), "ShufflePurifiedHand": bool(options.purified_hand.value),
"ShuffleDash": bool(world.dash_shuffle[player].value), "ShuffleDash": bool(options.dash_shuffle.value),
"ShuffleWallClimb": bool(world.wall_climb_shuffle[player].value), "ShuffleWallClimb": bool(options.wall_climb_shuffle.value),
"ShuffleSwordSkills": bool(world.skill_randomizer[player].value), "ShuffleSwordSkills": bool(options.skill_randomizer.value),
"ShuffleThorns": thorns, "ShuffleThorns": thorns,
"JunkLongQuests": bool(world.skip_long_quests[player].value), "JunkLongQuests": bool(options.skip_long_quests.value),
"StartWithWheel": bool(world.start_wheel[player].value), "StartWithWheel": bool(options.start_wheel.value),
"EnemyShuffleType": world.enemy_randomizer[player].value, "EnemyShuffleType": options.enemy_randomizer.value,
"MaintainClass": bool(world.enemy_groups[player].value), "MaintainClass": bool(options.enemy_groups.value),
"AreaScaling": bool(world.enemy_scaling[player].value), "AreaScaling": bool(options.enemy_scaling.value),
"BossShuffleType": 0, "BossShuffleType": 0,
"DoorShuffleType": 0 "DoorShuffleType": 0
@@ -385,8 +390,8 @@ class BlasphemousWorld(World):
"locations": locations, "locations": locations,
"doors": doors, "doors": doors,
"cfg": config, "cfg": config,
"ending": world.ending[self.player].value, "ending": options.ending.value,
"death_link": bool(world.death_link[self.player].value) "death_link": bool(options.death_link.value)
} }
return slot_data return slot_data

View File

@@ -4,7 +4,8 @@
# https://opensource.org/licenses/MIT # https://opensource.org/licenses/MIT
import typing import typing
from Options import Option, Range from Options import Option, Range, PerGameCommonOptions
from dataclasses import dataclass
class TaskAdvances(Range): class TaskAdvances(Range):
@@ -69,12 +70,12 @@ class KillerTrapWeight(Range):
default = 0 default = 0
bumpstik_options: typing.Dict[str, type(Option)] = { @dataclass
"task_advances": TaskAdvances, class BumpStikOptions(PerGameCommonOptions):
"turners": Turners, task_advances: TaskAdvances
"paint_cans": PaintCans, turners: Turners
"trap_count": Traps, paint_cans: PaintCans
"rainbow_trap_weight": RainbowTrapWeight, trap_count: Traps
"spinner_trap_weight": SpinnerTrapWeight, rainbow_trap_weight: RainbowTrapWeight
"killer_trap_weight": KillerTrapWeight spinner_trap_weight: SpinnerTrapWeight
} killer_trap_weight: KillerTrapWeight

View File

@@ -43,7 +43,8 @@ class BumpStikWorld(World):
required_client_version = (0, 3, 8) required_client_version = (0, 3, 8)
option_definitions = bumpstik_options options = BumpStikOptions
options_dataclass = BumpStikOptions
def __init__(self, world: MultiWorld, player: int): def __init__(self, world: MultiWorld, player: int):
super(BumpStikWorld, self).__init__(world, player) super(BumpStikWorld, self).__init__(world, player)
@@ -86,13 +87,13 @@ class BumpStikWorld(World):
return "Nothing" return "Nothing"
def generate_early(self): def generate_early(self):
self.task_advances = self.multiworld.task_advances[self.player].value self.task_advances = self.options.task_advances.value
self.turners = self.multiworld.turners[self.player].value self.turners = self.options.turners.value
self.paint_cans = self.multiworld.paint_cans[self.player].value self.paint_cans = self.options.paint_cans.value
self.traps = self.multiworld.trap_count[self.player].value self.traps = self.options.trap_count.value
self.rainbow_trap_weight = self.multiworld.rainbow_trap_weight[self.player].value self.rainbow_trap_weight = self.options.rainbow_trap_weight.value
self.spinner_trap_weight = self.multiworld.spinner_trap_weight[self.player].value self.spinner_trap_weight = self.options.spinner_trap_weight.value
self.killer_trap_weight = self.multiworld.killer_trap_weight[self.player].value self.killer_trap_weight = self.options.killer_trap_weight.value
def create_regions(self): def create_regions(self):
create_regions(self.multiworld, self.player) create_regions(self.multiworld, self.player)

View File

@@ -1,6 +1,7 @@
import typing import typing
from Options import Option from Options import Option, PerGameCommonOptions
from dataclasses import dataclass
@dataclass
checksfinder_options: typing.Dict[str, type(Option)] = { class ChecksFinderOptions(PerGameCommonOptions):
} pass

View File

@@ -1,9 +1,10 @@
from BaseClasses import Region, Entrance, Item, Tutorial, ItemClassification from BaseClasses import Region, Entrance, Item, Tutorial, ItemClassification
from .Items import ChecksFinderItem, item_table, required_items from .Items import ChecksFinderItem, item_table, required_items
from .Locations import ChecksFinderAdvancement, advancement_table, exclusion_table from .Locations import ChecksFinderAdvancement, advancement_table, exclusion_table
from .Options import checksfinder_options from .Options import ChecksFinderOptions
from .Rules import set_rules, set_completion_rules from .Rules import set_rules, set_completion_rules
from ..AutoWorld import World, WebWorld from ..AutoWorld import World, WebWorld
from dataclasses import fields
client_version = 7 client_version = 7
@@ -26,7 +27,8 @@ class ChecksFinderWorld(World):
with the mines! You win when you get all your items and beat the board! with the mines! You win when you get all your items and beat the board!
""" """
game: str = "ChecksFinder" game: str = "ChecksFinder"
option_definitions = checksfinder_options options = ChecksFinderOptions
options_dataclass = ChecksFinderOptions
topology_present = True topology_present = True
web = ChecksFinderWeb() web = ChecksFinderWeb()
@@ -79,8 +81,8 @@ class ChecksFinderWorld(World):
def fill_slot_data(self): def fill_slot_data(self):
slot_data = self._get_checksfinder_data() slot_data = self._get_checksfinder_data()
for option_name in checksfinder_options: for option_name in [field.name for field in fields(ChecksFinderOptions)]:
option = getattr(self.multiworld, option_name)[self.player] option = getattr(self.options, option_name)
if slot_data.get(option_name, None) is None and type(option.value) in {str, int}: if slot_data.get(option_name, None) is None and type(option.value) in {str, int}:
slot_data[option_name] = int(option.value) slot_data[option_name] = int(option.value)
return slot_data return slot_data

View File

@@ -1,6 +1,7 @@
from typing import Callable, Dict, NamedTuple, Optional from typing import Callable, Dict, NamedTuple, Optional
from BaseClasses import Item, ItemClassification, MultiWorld from BaseClasses import Item, ItemClassification
from .Options import CliqueOptions
class CliqueItem(Item): class CliqueItem(Item):
@@ -10,7 +11,7 @@ class CliqueItem(Item):
class CliqueItemData(NamedTuple): class CliqueItemData(NamedTuple):
code: Optional[int] = None code: Optional[int] = None
type: ItemClassification = ItemClassification.filler type: ItemClassification = ItemClassification.filler
can_create: Callable[[MultiWorld, int], bool] = lambda multiworld, player: True can_create: Callable[[CliqueOptions], bool] = lambda options: True
item_data_table: Dict[str, CliqueItemData] = { item_data_table: Dict[str, CliqueItemData] = {
@@ -21,11 +22,11 @@ item_data_table: Dict[str, CliqueItemData] = {
"Button Activation": CliqueItemData( "Button Activation": CliqueItemData(
code=69696968, code=69696968,
type=ItemClassification.progression, type=ItemClassification.progression,
can_create=lambda multiworld, player: bool(getattr(multiworld, "hard_mode")[player]), can_create=lambda options: bool(getattr(options, "hard_mode")),
), ),
"A Cool Filler Item (No Satisfaction Guaranteed)": CliqueItemData( "A Cool Filler Item (No Satisfaction Guaranteed)": CliqueItemData(
code=69696967, code=69696967,
can_create=lambda multiworld, player: False # Only created from `get_filler_item_name`. can_create=lambda options: False # Only created from `get_filler_item_name`.
), ),
"The Urge to Push": CliqueItemData( "The Urge to Push": CliqueItemData(
type=ItemClassification.progression, type=ItemClassification.progression,

View File

@@ -1,6 +1,8 @@
from typing import Callable, Dict, NamedTuple, Optional from typing import Callable, Dict, NamedTuple, Optional
from BaseClasses import Location, MultiWorld from BaseClasses import Location
from .Options import CliqueOptions
class CliqueLocation(Location): class CliqueLocation(Location):
@@ -10,7 +12,7 @@ class CliqueLocation(Location):
class CliqueLocationData(NamedTuple): class CliqueLocationData(NamedTuple):
region: str region: str
address: Optional[int] = None address: Optional[int] = None
can_create: Callable[[MultiWorld, int], bool] = lambda multiworld, player: True can_create: Callable[[CliqueOptions], bool] = lambda options: True
locked_item: Optional[str] = None locked_item: Optional[str] = None
@@ -22,7 +24,7 @@ location_data_table: Dict[str, CliqueLocationData] = {
"The Item on the Desk": CliqueLocationData( "The Item on the Desk": CliqueLocationData(
region="The Button Realm", region="The Button Realm",
address=69696968, address=69696968,
can_create=lambda multiworld, player: bool(getattr(multiworld, "hard_mode")[player]), can_create=lambda options: bool(getattr(options, "hard_mode")),
), ),
"In the Player's Mind": CliqueLocationData( "In the Player's Mind": CliqueLocationData(
region="The Button Realm", region="The Button Realm",

View File

@@ -1,6 +1,7 @@
from typing import Dict from typing import Dict
from Options import Choice, Option, Toggle from Options import Choice, Option, Toggle, PerGameCommonOptions
from dataclasses import dataclass
class HardMode(Toggle): class HardMode(Toggle):
@@ -25,10 +26,12 @@ class ButtonColor(Choice):
option_black = 11 option_black = 11
clique_options: Dict[str, type(Option)] = {
"color": ButtonColor, @dataclass
"hard_mode": HardMode, class CliqueOptions(PerGameCommonOptions):
color: ButtonColor
hard_mode: HardMode
# DeathLink is always on. Always. # DeathLink is always on. Always.
# "death_link": DeathLink, # death_link: DeathLink
}

View File

@@ -1,10 +1,11 @@
from typing import Callable from typing import Callable
from BaseClasses import CollectionState, MultiWorld from BaseClasses import CollectionState
from .Options import CliqueOptions
def get_button_rule(multiworld: MultiWorld, player: int) -> Callable[[CollectionState], bool]: def get_button_rule(options: CliqueOptions, player: int) -> Callable[[CollectionState], bool]:
if getattr(multiworld, "hard_mode")[player]: if getattr(options, "hard_mode"):
return lambda state: state.has("Button Activation", player) return lambda state: state.has("Button Activation", player)
return lambda state: True return lambda state: True

View File

@@ -4,7 +4,7 @@ from BaseClasses import Region, Tutorial
from worlds.AutoWorld import WebWorld, World from worlds.AutoWorld import WebWorld, World
from .Items import CliqueItem, item_data_table, item_table from .Items import CliqueItem, item_data_table, item_table
from .Locations import CliqueLocation, location_data_table, location_table, locked_locations from .Locations import CliqueLocation, location_data_table, location_table, locked_locations
from .Options import clique_options from .Options import CliqueOptions
from .Regions import region_data_table from .Regions import region_data_table
from .Rules import get_button_rule from .Rules import get_button_rule
@@ -29,7 +29,8 @@ class CliqueWorld(World):
game = "Clique" game = "Clique"
data_version = 3 data_version = 3
web = CliqueWebWorld() web = CliqueWebWorld()
option_definitions = clique_options options = CliqueOptions
options_dataclass = CliqueOptions
location_name_to_id = location_table location_name_to_id = location_table
item_name_to_id = item_table item_name_to_id = item_table
@@ -39,7 +40,7 @@ class CliqueWorld(World):
def create_items(self) -> None: def create_items(self) -> None:
item_pool: List[CliqueItem] = [] item_pool: List[CliqueItem] = []
for name, item in item_data_table.items(): for name, item in item_data_table.items():
if item.code and item.can_create(self.multiworld, self.player): if item.code and item.can_create(self.options):
item_pool.append(self.create_item(name)) item_pool.append(self.create_item(name))
self.multiworld.itempool += item_pool self.multiworld.itempool += item_pool
@@ -55,27 +56,27 @@ class CliqueWorld(World):
region = self.multiworld.get_region(region_name, self.player) region = self.multiworld.get_region(region_name, self.player)
region.add_locations({ region.add_locations({
location_name: location_data.address for location_name, location_data in location_data_table.items() location_name: location_data.address for location_name, location_data in location_data_table.items()
if location_data.region == region_name and location_data.can_create(self.multiworld, self.player) if location_data.region == region_name and location_data.can_create(self.options)
}, CliqueLocation) }, CliqueLocation)
region.add_exits(region_data_table[region_name].connecting_regions) region.add_exits(region_data_table[region_name].connecting_regions)
# Place locked locations. # Place locked locations.
for location_name, location_data in locked_locations.items(): for location_name, location_data in locked_locations.items():
# Ignore locations we never created. # Ignore locations we never created.
if not location_data.can_create(self.multiworld, self.player): if not location_data.can_create(self.options):
continue continue
locked_item = self.create_item(location_data_table[location_name].locked_item) locked_item = self.create_item(location_data_table[location_name].locked_item)
self.multiworld.get_location(location_name, self.player).place_locked_item(locked_item) self.multiworld.get_location(location_name, self.player).place_locked_item(locked_item)
# Set priority location for the Big Red Button! # Set priority location for the Big Red Button!
self.multiworld.priority_locations[self.player].value.add("The Big Red Button") self.options.priority_locations.value.add("The Big Red Button")
def get_filler_item_name(self) -> str: def get_filler_item_name(self) -> str:
return "A Cool Filler Item (No Satisfaction Guaranteed)" return "A Cool Filler Item (No Satisfaction Guaranteed)"
def set_rules(self) -> None: def set_rules(self) -> None:
button_rule = get_button_rule(self.multiworld, self.player) button_rule = get_button_rule(self.options, self.player)
self.multiworld.get_location("The Big Red Button", self.player).access_rule = button_rule self.multiworld.get_location("The Big Red Button", self.player).access_rule = button_rule
self.multiworld.get_location("In the Player's Mind", self.player).access_rule = button_rule self.multiworld.get_location("In the Player's Mind", self.player).access_rule = button_rule
@@ -88,5 +89,5 @@ class CliqueWorld(World):
def fill_slot_data(self): def fill_slot_data(self):
return { return {
"color": getattr(self.multiworld, "color")[self.player].current_key "color": getattr(self.options, "color").current_key
} }

View File

@@ -1,6 +1,7 @@
import typing import typing
from Options import Toggle, DefaultOnToggle, Option, Range, Choice, ItemDict, DeathLink from Options import Toggle, DefaultOnToggle, Option, Range, Choice, ItemDict, DeathLink, PerGameCommonOptions
from dataclasses import dataclass
class RandomizeWeaponLocations(DefaultOnToggle): class RandomizeWeaponLocations(DefaultOnToggle):
@@ -200,36 +201,36 @@ class EnableDLCOption(Toggle):
display_name = "Enable DLC" display_name = "Enable DLC"
dark_souls_options: typing.Dict[str, Option] = { @dataclass
"enable_weapon_locations": RandomizeWeaponLocations, class DarkSouls3Options(PerGameCommonOptions):
"enable_shield_locations": RandomizeShieldLocations, enable_weapon_locations: RandomizeWeaponLocations
"enable_armor_locations": RandomizeArmorLocations, enable_shield_locations: RandomizeShieldLocations
"enable_ring_locations": RandomizeRingLocations, enable_armor_locations: RandomizeArmorLocations
"enable_spell_locations": RandomizeSpellLocations, enable_ring_locations: RandomizeRingLocations
"enable_key_locations": RandomizeKeyLocations, enable_spell_locations: RandomizeSpellLocations
"enable_boss_locations": RandomizeBossSoulLocations, enable_key_locations: RandomizeKeyLocations
"enable_npc_locations": RandomizeNPCLocations, enable_boss_locations: RandomizeBossSoulLocations
"enable_misc_locations": RandomizeMiscLocations, enable_npc_locations: RandomizeNPCLocations
"enable_health_upgrade_locations": RandomizeHealthLocations, enable_misc_locations: RandomizeMiscLocations
"enable_progressive_locations": RandomizeProgressiveLocationsOption, enable_health_upgrade_locations: RandomizeHealthLocations
"pool_type": PoolTypeOption, enable_progressive_locations: RandomizeProgressiveLocationsOption
"guaranteed_items": GuaranteedItemsOption, pool_type: PoolTypeOption
"auto_equip": AutoEquipOption, guaranteed_items: GuaranteedItemsOption
"lock_equip": LockEquipOption, auto_equip: AutoEquipOption
"no_weapon_requirements": NoWeaponRequirementsOption, lock_equip: LockEquipOption
"randomize_infusion": RandomizeInfusionOption, no_weapon_requirements: NoWeaponRequirementsOption
"randomize_infusion_percentage": RandomizeInfusionPercentageOption, randomize_infusion: RandomizeInfusionOption
"randomize_weapon_level": RandomizeWeaponLevelOption, randomize_infusion_percentage: RandomizeInfusionPercentageOption
"randomize_weapon_level_percentage": RandomizeWeaponLevelPercentageOption, randomize_weapon_level: RandomizeWeaponLevelOption
"min_levels_in_5": MinLevelsIn5WeaponPoolOption, randomize_weapon_level_percentage: RandomizeWeaponLevelPercentageOption
"max_levels_in_5": MaxLevelsIn5WeaponPoolOption, min_levels_in_5: MinLevelsIn5WeaponPoolOption
"min_levels_in_10": MinLevelsIn10WeaponPoolOption, max_levels_in_5: MaxLevelsIn5WeaponPoolOption
"max_levels_in_10": MaxLevelsIn10WeaponPoolOption, min_levels_in_10: MinLevelsIn10WeaponPoolOption
"early_banner": EarlySmallLothricBanner, max_levels_in_10: MaxLevelsIn10WeaponPoolOption
"late_basin_of_vows": LateBasinOfVowsOption, early_banner: EarlySmallLothricBanner
"late_dlc": LateDLCOption, late_basin_of_vows: LateBasinOfVowsOption
"no_spell_requirements": NoSpellRequirementsOption, late_dlc: LateDLCOption
"no_equip_load": NoEquipLoadOption, no_spell_requirements: NoSpellRequirementsOption
"death_link": DeathLink, no_equip_load: NoEquipLoadOption
"enable_dlc": EnableDLCOption, death_link: DeathLink
} enable_dlc: EnableDLCOption

View File

@@ -9,7 +9,7 @@ from worlds.generic.Rules import set_rule, add_rule, add_item_rule
from .Items import DarkSouls3Item, DS3ItemCategory, item_dictionary, key_item_names, item_descriptions from .Items import DarkSouls3Item, DS3ItemCategory, item_dictionary, key_item_names, item_descriptions
from .Locations import DarkSouls3Location, DS3LocationCategory, location_tables, location_dictionary from .Locations import DarkSouls3Location, DS3LocationCategory, location_tables, location_dictionary
from .Options import RandomizeWeaponLevelOption, PoolTypeOption, EarlySmallLothricBanner, dark_souls_options from .Options import RandomizeWeaponLevelOption, PoolTypeOption, EarlySmallLothricBanner, DarkSouls3Options
class DarkSouls3Web(WebWorld): class DarkSouls3Web(WebWorld):
@@ -43,7 +43,8 @@ class DarkSouls3World(World):
""" """
game: str = "Dark Souls III" game: str = "Dark Souls III"
option_definitions = dark_souls_options options = DarkSouls3Options
options_dataclass = DarkSouls3Options
topology_present: bool = True topology_present: bool = True
web = DarkSouls3Web() web = DarkSouls3Web()
data_version = 8 data_version = 8
@@ -72,47 +73,47 @@ class DarkSouls3World(World):
def generate_early(self): def generate_early(self):
if self.multiworld.enable_weapon_locations[self.player] == Toggle.option_true: if self.options.enable_weapon_locations == Toggle.option_true:
self.enabled_location_categories.add(DS3LocationCategory.WEAPON) self.enabled_location_categories.add(DS3LocationCategory.WEAPON)
if self.multiworld.enable_shield_locations[self.player] == Toggle.option_true: if self.options.enable_shield_locations == Toggle.option_true:
self.enabled_location_categories.add(DS3LocationCategory.SHIELD) self.enabled_location_categories.add(DS3LocationCategory.SHIELD)
if self.multiworld.enable_armor_locations[self.player] == Toggle.option_true: if self.options.enable_armor_locations == Toggle.option_true:
self.enabled_location_categories.add(DS3LocationCategory.ARMOR) self.enabled_location_categories.add(DS3LocationCategory.ARMOR)
if self.multiworld.enable_ring_locations[self.player] == Toggle.option_true: if self.options.enable_ring_locations == Toggle.option_true:
self.enabled_location_categories.add(DS3LocationCategory.RING) self.enabled_location_categories.add(DS3LocationCategory.RING)
if self.multiworld.enable_spell_locations[self.player] == Toggle.option_true: if self.options.enable_spell_locations == Toggle.option_true:
self.enabled_location_categories.add(DS3LocationCategory.SPELL) self.enabled_location_categories.add(DS3LocationCategory.SPELL)
if self.multiworld.enable_npc_locations[self.player] == Toggle.option_true: if self.options.enable_npc_locations == Toggle.option_true:
self.enabled_location_categories.add(DS3LocationCategory.NPC) self.enabled_location_categories.add(DS3LocationCategory.NPC)
if self.multiworld.enable_key_locations[self.player] == Toggle.option_true: if self.options.enable_key_locations == Toggle.option_true:
self.enabled_location_categories.add(DS3LocationCategory.KEY) self.enabled_location_categories.add(DS3LocationCategory.KEY)
if self.multiworld.early_banner[self.player] == EarlySmallLothricBanner.option_early_global: if self.options.early_banner == EarlySmallLothricBanner.option_early_global:
self.multiworld.early_items[self.player]['Small Lothric Banner'] = 1 self.options.early_items['Small Lothric Banner'] = 1
elif self.multiworld.early_banner[self.player] == EarlySmallLothricBanner.option_early_local: elif self.options.early_banner == EarlySmallLothricBanner.option_early_local:
self.multiworld.local_early_items[self.player]['Small Lothric Banner'] = 1 self.options.local_early_items['Small Lothric Banner'] = 1
if self.multiworld.enable_boss_locations[self.player] == Toggle.option_true: if self.options.enable_boss_locations == Toggle.option_true:
self.enabled_location_categories.add(DS3LocationCategory.BOSS) self.enabled_location_categories.add(DS3LocationCategory.BOSS)
if self.multiworld.enable_misc_locations[self.player] == Toggle.option_true: if self.options.enable_misc_locations == Toggle.option_true:
self.enabled_location_categories.add(DS3LocationCategory.MISC) self.enabled_location_categories.add(DS3LocationCategory.MISC)
if self.multiworld.enable_health_upgrade_locations[self.player] == Toggle.option_true: if self.options.enable_health_upgrade_locations == Toggle.option_true:
self.enabled_location_categories.add(DS3LocationCategory.HEALTH) self.enabled_location_categories.add(DS3LocationCategory.HEALTH)
if self.multiworld.enable_progressive_locations[self.player] == Toggle.option_true: if self.options.enable_progressive_locations == Toggle.option_true:
self.enabled_location_categories.add(DS3LocationCategory.PROGRESSIVE_ITEM) self.enabled_location_categories.add(DS3LocationCategory.PROGRESSIVE_ITEM)
def create_regions(self): def create_regions(self):
progressive_location_table = [] progressive_location_table = []
if self.multiworld.enable_progressive_locations[self.player]: if self.options.enable_progressive_locations:
progressive_location_table = [] + \ progressive_location_table = [] + \
location_tables["Progressive Items 1"] + \ location_tables["Progressive Items 1"] + \
location_tables["Progressive Items 2"] + \ location_tables["Progressive Items 2"] + \
location_tables["Progressive Items 3"] + \ location_tables["Progressive Items 3"] + \
location_tables["Progressive Items 4"] location_tables["Progressive Items 4"]
if self.multiworld.enable_dlc[self.player].value: if self.options.enable_dlc.value:
progressive_location_table += location_tables["Progressive Items DLC"] progressive_location_table += location_tables["Progressive Items DLC"]
if self.multiworld.enable_health_upgrade_locations[self.player]: if self.options.enable_health_upgrade_locations:
progressive_location_table += location_tables["Progressive Items Health"] progressive_location_table += location_tables["Progressive Items Health"]
# Create Vanilla Regions # Create Vanilla Regions
@@ -146,7 +147,7 @@ class DarkSouls3World(World):
regions["Consumed King's Garden"].locations.append(potd_location) regions["Consumed King's Garden"].locations.append(potd_location)
# Create DLC Regions # Create DLC Regions
if self.multiworld.enable_dlc[self.player]: if self.options.enable_dlc:
regions.update({region_name: self.create_region(region_name, location_tables[region_name]) for region_name in [ regions.update({region_name: self.create_region(region_name, location_tables[region_name]) for region_name in [
"Painted World of Ariandel 1", "Painted World of Ariandel 1",
"Painted World of Ariandel 2", "Painted World of Ariandel 2",
@@ -192,7 +193,7 @@ class DarkSouls3World(World):
create_connection("Consumed King's Garden", "Untended Graves") create_connection("Consumed King's Garden", "Untended Graves")
# Connect DLC Regions # Connect DLC Regions
if self.multiworld.enable_dlc[self.player]: if self.options.enable_dlc:
create_connection("Cathedral of the Deep", "Painted World of Ariandel 1") create_connection("Cathedral of the Deep", "Painted World of Ariandel 1")
create_connection("Painted World of Ariandel 1", "Painted World of Ariandel 2") create_connection("Painted World of Ariandel 1", "Painted World of Ariandel 2")
create_connection("Painted World of Ariandel 2", "Dreg Heap") create_connection("Painted World of Ariandel 2", "Dreg Heap")
@@ -240,7 +241,7 @@ class DarkSouls3World(World):
def create_items(self): def create_items(self):
dlc_enabled = self.multiworld.enable_dlc[self.player] == Toggle.option_true dlc_enabled = self.options.enable_dlc == Toggle.option_true
itempool_by_category = {category: [] for category in self.enabled_location_categories} itempool_by_category = {category: [] for category in self.enabled_location_categories}
@@ -254,7 +255,7 @@ class DarkSouls3World(World):
itempool_by_category[location.category].append(location.default_item_name) itempool_by_category[location.category].append(location.default_item_name)
# Replace each item category with a random sample of items of those types # Replace each item category with a random sample of items of those types
if self.multiworld.pool_type[self.player] == PoolTypeOption.option_various: if self.options.pool_type == PoolTypeOption.option_various:
def create_random_replacement_list(item_categories: Set[DS3ItemCategory], num_items: int): def create_random_replacement_list(item_categories: Set[DS3ItemCategory], num_items: int):
candidates = [ candidates = [
item.name for item item.name for item
@@ -300,7 +301,7 @@ class DarkSouls3World(World):
# A list of items we can replace # A list of items we can replace
removable_items = [item for item in itempool if item.classification != ItemClassification.progression] removable_items = [item for item in itempool if item.classification != ItemClassification.progression]
guaranteed_items = self.multiworld.guaranteed_items[self.player].value guaranteed_items = self.options.guaranteed_items.value
for item_name in guaranteed_items: for item_name in guaranteed_items:
# Break early just in case nothing is removable (if user is trying to guarantee more # Break early just in case nothing is removable (if user is trying to guarantee more
# items than the pool can hold, for example) # items than the pool can hold, for example)
@@ -384,22 +385,22 @@ class DarkSouls3World(World):
state.has("Cinders of a Lord - Aldrich", self.player) and state.has("Cinders of a Lord - Aldrich", self.player) and
state.has("Cinders of a Lord - Lothric Prince", self.player)) state.has("Cinders of a Lord - Lothric Prince", self.player))
if self.multiworld.late_basin_of_vows[self.player] == Toggle.option_true: if self.options.late_basin_of_vows == Toggle.option_true:
add_rule(self.multiworld.get_entrance("Go To Lothric Castle", self.player), add_rule(self.multiworld.get_entrance("Go To Lothric Castle", self.player),
lambda state: state.has("Small Lothric Banner", self.player)) lambda state: state.has("Small Lothric Banner", self.player))
# DLC Access Rules Below # DLC Access Rules Below
if self.multiworld.enable_dlc[self.player]: if self.options.enable_dlc:
set_rule(self.multiworld.get_entrance("Go To Ringed City", self.player), set_rule(self.multiworld.get_entrance("Go To Ringed City", self.player),
lambda state: state.has("Small Envoy Banner", self.player)) lambda state: state.has("Small Envoy Banner", self.player))
# If key items are randomized, must have contraption key to enter second half of Ashes DLC # If key items are randomized, must have contraption key to enter second half of Ashes DLC
# If key items are not randomized, Contraption Key is guaranteed to be accessible before it is needed # If key items are not randomized, Contraption Key is guaranteed to be accessible before it is needed
if self.multiworld.enable_key_locations[self.player] == Toggle.option_true: if self.options.enable_key_locations == Toggle.option_true:
add_rule(self.multiworld.get_entrance("Go To Painted World of Ariandel 2", self.player), add_rule(self.multiworld.get_entrance("Go To Painted World of Ariandel 2", self.player),
lambda state: state.has("Contraption Key", self.player)) lambda state: state.has("Contraption Key", self.player))
if self.multiworld.late_dlc[self.player] == Toggle.option_true: if self.options.late_dlc == Toggle.option_true:
add_rule(self.multiworld.get_entrance("Go To Painted World of Ariandel 1", self.player), add_rule(self.multiworld.get_entrance("Go To Painted World of Ariandel 1", self.player),
lambda state: state.has("Small Doll", self.player)) lambda state: state.has("Small Doll", self.player))
@@ -407,7 +408,7 @@ class DarkSouls3World(World):
set_rule(self.multiworld.get_location("PC: Cinders of a Lord - Yhorm the Giant", self.player), set_rule(self.multiworld.get_location("PC: Cinders of a Lord - Yhorm the Giant", self.player),
lambda state: state.has("Storm Ruler", self.player)) lambda state: state.has("Storm Ruler", self.player))
if self.multiworld.enable_ring_locations[self.player] == Toggle.option_true: if self.options.enable_ring_locations == Toggle.option_true:
set_rule(self.multiworld.get_location("ID: Bellowing Dragoncrest Ring", self.player), set_rule(self.multiworld.get_location("ID: Bellowing Dragoncrest Ring", self.player),
lambda state: state.has("Jailbreaker's Key", self.player)) lambda state: state.has("Jailbreaker's Key", self.player))
set_rule(self.multiworld.get_location("ID: Covetous Gold Serpent Ring", self.player), set_rule(self.multiworld.get_location("ID: Covetous Gold Serpent Ring", self.player),
@@ -415,7 +416,7 @@ class DarkSouls3World(World):
set_rule(self.multiworld.get_location("UG: Hornet Ring", self.player), set_rule(self.multiworld.get_location("UG: Hornet Ring", self.player),
lambda state: state.has("Small Lothric Banner", self.player)) lambda state: state.has("Small Lothric Banner", self.player))
if self.multiworld.enable_npc_locations[self.player] == Toggle.option_true: if self.options.enable_npc_locations == Toggle.option_true:
set_rule(self.multiworld.get_location("HWL: Greirat's Ashes", self.player), set_rule(self.multiworld.get_location("HWL: Greirat's Ashes", self.player),
lambda state: state.has("Cell Key", self.player)) lambda state: state.has("Cell Key", self.player))
set_rule(self.multiworld.get_location("HWL: Blue Tearstone Ring", self.player), set_rule(self.multiworld.get_location("HWL: Blue Tearstone Ring", self.player),
@@ -431,11 +432,11 @@ class DarkSouls3World(World):
set_rule(self.multiworld.get_location("ID: Karla's Trousers", self.player), set_rule(self.multiworld.get_location("ID: Karla's Trousers", self.player),
lambda state: state.has("Jailer's Key Ring", self.player)) lambda state: state.has("Jailer's Key Ring", self.player))
if self.multiworld.enable_misc_locations[self.player] == Toggle.option_true: if self.options.enable_misc_locations == Toggle.option_true:
set_rule(self.multiworld.get_location("ID: Prisoner Chief's Ashes", self.player), set_rule(self.multiworld.get_location("ID: Prisoner Chief's Ashes", self.player),
lambda state: state.has("Jailer's Key Ring", self.player)) lambda state: state.has("Jailer's Key Ring", self.player))
if self.multiworld.enable_boss_locations[self.player] == Toggle.option_true: if self.options.enable_boss_locations == Toggle.option_true:
set_rule(self.multiworld.get_location("PC: Soul of Yhorm the Giant", self.player), set_rule(self.multiworld.get_location("PC: Soul of Yhorm the Giant", self.player),
lambda state: state.has("Storm Ruler", self.player)) lambda state: state.has("Storm Ruler", self.player))
set_rule(self.multiworld.get_location("HWL: Soul of the Dancer", self.player), set_rule(self.multiworld.get_location("HWL: Soul of the Dancer", self.player),
@@ -443,7 +444,7 @@ class DarkSouls3World(World):
# Lump Soul of the Dancer in with LC for locations that should not be reachable # Lump Soul of the Dancer in with LC for locations that should not be reachable
# before having access to US. (Prevents requiring getting Basin to fight Dancer to get SLB to go to US) # before having access to US. (Prevents requiring getting Basin to fight Dancer to get SLB to go to US)
if self.multiworld.late_basin_of_vows[self.player] == Toggle.option_true: if self.options.late_basin_of_vows == Toggle.option_true:
add_rule(self.multiworld.get_location("HWL: Soul of the Dancer", self.player), add_rule(self.multiworld.get_location("HWL: Soul of the Dancer", self.player),
lambda state: state.has("Small Lothric Banner", self.player)) lambda state: state.has("Small Lothric Banner", self.player))
@@ -453,10 +454,10 @@ class DarkSouls3World(World):
set_rule(self.multiworld.get_location("LC: Grand Archives Key", self.player), gotthard_corpse_rule) set_rule(self.multiworld.get_location("LC: Grand Archives Key", self.player), gotthard_corpse_rule)
if self.multiworld.enable_weapon_locations[self.player] == Toggle.option_true: if self.options.enable_weapon_locations == Toggle.option_true:
set_rule(self.multiworld.get_location("LC: Gotthard Twinswords", self.player), gotthard_corpse_rule) set_rule(self.multiworld.get_location("LC: Gotthard Twinswords", self.player), gotthard_corpse_rule)
self.multiworld.completion_condition[self.player] = lambda state: \ self.options.completion_condition = lambda state: \
state.has("Cinders of a Lord - Abyss Watcher", self.player) and \ state.has("Cinders of a Lord - Abyss Watcher", self.player) and \
state.has("Cinders of a Lord - Yhorm the Giant", self.player) and \ state.has("Cinders of a Lord - Yhorm the Giant", self.player) and \
state.has("Cinders of a Lord - Aldrich", self.player) and \ state.has("Cinders of a Lord - Aldrich", self.player) and \
@@ -470,13 +471,13 @@ class DarkSouls3World(World):
name_to_ds3_code = {item.name: item.ds3_code for item in item_dictionary.values()} name_to_ds3_code = {item.name: item.ds3_code for item in item_dictionary.values()}
# Randomize some weapon upgrades # Randomize some weapon upgrades
if self.multiworld.randomize_weapon_level[self.player] != RandomizeWeaponLevelOption.option_none: if self.options.randomize_weapon_level != RandomizeWeaponLevelOption.option_none:
# if the user made an error and set a min higher than the max we default to the max # if the user made an error and set a min higher than the max we default to the max
max_5 = self.multiworld.max_levels_in_5[self.player] max_5 = self.options.max_levels_in_5
min_5 = min(self.multiworld.min_levels_in_5[self.player], max_5) min_5 = min(self.options.min_levels_in_5, max_5)
max_10 = self.multiworld.max_levels_in_10[self.player] max_10 = self.options.max_levels_in_10
min_10 = min(self.multiworld.min_levels_in_10[self.player], max_10) min_10 = min(self.options.min_levels_in_10, max_10)
weapon_level_percentage = self.multiworld.randomize_weapon_level_percentage[self.player] weapon_level_percentage = self.options.randomize_weapon_level_percentage
for item in item_dictionary.values(): for item in item_dictionary.values():
if self.multiworld.per_slot_randoms[self.player].randint(0, 99) < weapon_level_percentage: if self.multiworld.per_slot_randoms[self.player].randint(0, 99) < weapon_level_percentage:
@@ -486,8 +487,8 @@ class DarkSouls3World(World):
name_to_ds3_code[item.name] += self.multiworld.per_slot_randoms[self.player].randint(min_10, max_10) name_to_ds3_code[item.name] += self.multiworld.per_slot_randoms[self.player].randint(min_10, max_10)
# Randomize some weapon infusions # Randomize some weapon infusions
if self.multiworld.randomize_infusion[self.player] == Toggle.option_true: if self.options.randomize_infusion == Toggle.option_true:
infusion_percentage = self.multiworld.randomize_infusion_percentage[self.player] infusion_percentage = self.options.randomize_infusion_percentage
for item in item_dictionary.values(): for item in item_dictionary.values():
if item.category in {DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE, DS3ItemCategory.SHIELD_INFUSIBLE}: if item.category in {DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE, DS3ItemCategory.SHIELD_INFUSIBLE}:
if self.multiworld.per_slot_randoms[self.player].randint(0, 99) < infusion_percentage: if self.multiworld.per_slot_randoms[self.player].randint(0, 99) < infusion_percentage:
@@ -518,22 +519,22 @@ class DarkSouls3World(World):
slot_data = { slot_data = {
"options": { "options": {
"enable_weapon_locations": self.multiworld.enable_weapon_locations[self.player].value, "enable_weapon_locations": self.options.enable_weapon_locations.value,
"enable_shield_locations": self.multiworld.enable_shield_locations[self.player].value, "enable_shield_locations": self.options.enable_shield_locations.value,
"enable_armor_locations": self.multiworld.enable_armor_locations[self.player].value, "enable_armor_locations": self.options.enable_armor_locations.value,
"enable_ring_locations": self.multiworld.enable_ring_locations[self.player].value, "enable_ring_locations": self.options.enable_ring_locations.value,
"enable_spell_locations": self.multiworld.enable_spell_locations[self.player].value, "enable_spell_locations": self.options.enable_spell_locations.value,
"enable_key_locations": self.multiworld.enable_key_locations[self.player].value, "enable_key_locations": self.options.enable_key_locations.value,
"enable_boss_locations": self.multiworld.enable_boss_locations[self.player].value, "enable_boss_locations": self.options.enable_boss_locations.value,
"enable_npc_locations": self.multiworld.enable_npc_locations[self.player].value, "enable_npc_locations": self.options.enable_npc_locations.value,
"enable_misc_locations": self.multiworld.enable_misc_locations[self.player].value, "enable_misc_locations": self.options.enable_misc_locations.value,
"auto_equip": self.multiworld.auto_equip[self.player].value, "auto_equip": self.options.auto_equip.value,
"lock_equip": self.multiworld.lock_equip[self.player].value, "lock_equip": self.options.lock_equip.value,
"no_weapon_requirements": self.multiworld.no_weapon_requirements[self.player].value, "no_weapon_requirements": self.options.no_weapon_requirements.value,
"death_link": self.multiworld.death_link[self.player].value, "death_link": self.options.death_link.value,
"no_spell_requirements": self.multiworld.no_spell_requirements[self.player].value, "no_spell_requirements": self.options.no_spell_requirements.value,
"no_equip_load": self.multiworld.no_equip_load[self.player].value, "no_equip_load": self.options.no_equip_load.value,
"enable_dlc": self.multiworld.enable_dlc[self.player].value "enable_dlc": self.options.enable_dlc.value
}, },
"seed": self.multiworld.seed_name, # to verify the server's multiworld "seed": self.multiworld.seed_name, # to verify the server's multiworld
"slot": self.multiworld.player_name[self.player], # to connect to server "slot": self.multiworld.player_name[self.player], # to connect to server

View File

@@ -6,6 +6,8 @@ import shutil
import threading import threading
import zipfile import zipfile
from typing import Optional, TYPE_CHECKING, Any, List, Callable, Tuple, Union from typing import Optional, TYPE_CHECKING, Any, List, Callable, Tuple, Union
from dataclasses import fields
import datetime
import jinja2 import jinja2
@@ -88,6 +90,7 @@ class FactorioModFile(worlds.Files.APContainer):
def generate_mod(world: "Factorio", output_directory: str): def generate_mod(world: "Factorio", output_directory: str):
player = world.player player = world.player
multiworld = world.multiworld multiworld = world.multiworld
options = world.options
global data_final_template, locale_template, control_template, data_template, settings_template global data_final_template, locale_template, control_template, data_template, settings_template
with template_load_lock: with template_load_lock:
if not data_final_template: if not data_final_template:
@@ -129,40 +132,40 @@ def generate_mod(world: "Factorio", output_directory: str):
"base_tech_table": base_tech_table, "base_tech_table": base_tech_table,
"tech_to_progressive_lookup": tech_to_progressive_lookup, "tech_to_progressive_lookup": tech_to_progressive_lookup,
"mod_name": mod_name, "mod_name": mod_name,
"allowed_science_packs": multiworld.max_science_pack[player].get_allowed_packs(), "allowed_science_packs": options.max_science_pack.get_allowed_packs(),
"custom_technologies": multiworld.worlds[player].custom_technologies, "custom_technologies": multiworld.worlds[player].custom_technologies,
"tech_tree_layout_prerequisites": world.tech_tree_layout_prerequisites, "tech_tree_layout_prerequisites": world.tech_tree_layout_prerequisites,
"slot_name": multiworld.player_name[player], "seed_name": multiworld.seed_name, "slot_name": multiworld.player_name[player], "seed_name": multiworld.seed_name,
"slot_player": player, "slot_player": player,
"starting_items": multiworld.starting_items[player], "recipes": recipes, "starting_items": options.starting_items, "recipes": recipes,
"random": random, "flop_random": flop_random, "random": random, "flop_random": flop_random,
"recipe_time_scale": recipe_time_scales.get(multiworld.recipe_time[player].value, None), "recipe_time_scale": recipe_time_scales.get(options.recipe_time.value, None),
"recipe_time_range": recipe_time_ranges.get(multiworld.recipe_time[player].value, None), "recipe_time_range": recipe_time_ranges.get(options.recipe_time.value, None),
"free_sample_blacklist": {item: 1 for item in free_sample_exclusions}, "free_sample_blacklist": {item: 1 for item in free_sample_exclusions},
"progressive_technology_table": {tech.name: tech.progressive for tech in "progressive_technology_table": {tech.name: tech.progressive for tech in
progressive_technology_table.values()}, progressive_technology_table.values()},
"custom_recipes": world.custom_recipes, "custom_recipes": world.custom_recipes,
"max_science_pack": multiworld.max_science_pack[player].value, "max_science_pack": options.max_science_pack.value,
"liquids": fluids, "liquids": fluids,
"goal": multiworld.goal[player].value, "goal": options.goal.value,
"energy_link": multiworld.energy_link[player].value, "energy_link": options.energy_link.value,
"useless_technologies": useless_technologies, "useless_technologies": useless_technologies,
"chunk_shuffle": multiworld.chunk_shuffle[player].value if hasattr(multiworld, "chunk_shuffle") else 0, "chunk_shuffle": options.chunk_shuffle.value if datetime.datetime.today().month == 4 else 0,
} }
for factorio_option in Options.factorio_options: for factorio_option in [field.name for field in fields(Options.FactorioOptions)]:
if factorio_option in ["free_sample_blacklist", "free_sample_whitelist"]: if factorio_option in ["free_sample_blacklist", "free_sample_whitelist"]:
continue continue
template_data[factorio_option] = getattr(multiworld, factorio_option)[player].value template_data[factorio_option] = getattr(options, factorio_option).value
if getattr(multiworld, "silo")[player].value == Options.Silo.option_randomize_recipe: if getattr(options, "silo").value == Options.Silo.option_randomize_recipe:
template_data["free_sample_blacklist"]["rocket-silo"] = 1 template_data["free_sample_blacklist"]["rocket-silo"] = 1
if getattr(multiworld, "satellite")[player].value == Options.Satellite.option_randomize_recipe: if getattr(options, "satellite").value == Options.Satellite.option_randomize_recipe:
template_data["free_sample_blacklist"]["satellite"] = 1 template_data["free_sample_blacklist"]["satellite"] = 1
template_data["free_sample_blacklist"].update({item: 1 for item in multiworld.free_sample_blacklist[player].value}) template_data["free_sample_blacklist"].update({item: 1 for item in options.free_sample_blacklist.value})
template_data["free_sample_blacklist"].update({item: 0 for item in multiworld.free_sample_whitelist[player].value}) template_data["free_sample_blacklist"].update({item: 0 for item in options.free_sample_whitelist.value})
zf_path = os.path.join(output_directory, versioned_mod_name + ".zip") zf_path = os.path.join(output_directory, versioned_mod_name + ".zip")
mod = FactorioModFile(zf_path, player=player, player_name=multiworld.player_name[player]) mod = FactorioModFile(zf_path, player=player, player_name=multiworld.player_name[player])

View File

@@ -1,10 +1,10 @@
from __future__ import annotations from __future__ import annotations
import typing import typing
import datetime
from Options import Choice, OptionDict, OptionSet, Option, DefaultOnToggle, Range, DeathLink, Toggle, \ from Options import Choice, OptionDict, OptionSet, Option, DefaultOnToggle, Range, DeathLink, Toggle, \
StartInventoryPool StartInventoryPool, PerGameCommonOptions
from schema import Schema, Optional, And, Or from schema import Schema, Optional, And, Or
from dataclasses import dataclass
# schema helpers # schema helpers
FloatRange = lambda low, high: And(Or(int, float), lambda f: low <= f <= high) FloatRange = lambda low, high: And(Or(int, float), lambda f: low <= f <= high)
@@ -210,7 +210,7 @@ class RecipeIngredientsOffset(Range):
class FactorioStartItems(OptionDict): class FactorioStartItems(OptionDict):
"""Mapping of Factorio internal item-name to amount granted on start.""" """Mapping of Factorio internal item-name to amount granted on start."""
display_name = "Starting Items" display_name = "Starting Items"
default = {"burner-mining-drill": 4, "stone-furnace": 4, "raw-fish": 50} default = {"burner-mining-drill": 19, "stone-furnace": 19}
class FactorioFreeSampleBlacklist(OptionSet): class FactorioFreeSampleBlacklist(OptionSet):
@@ -422,50 +422,44 @@ class EnergyLink(Toggle):
display_name = "EnergyLink" display_name = "EnergyLink"
factorio_options: typing.Dict[str, type(Option)] = { class ChunkShuffle(Toggle):
"max_science_pack": MaxSciencePack, """Entrance Randomizer.
"goal": Goal, 2023 April Fool's option. Shuffles chunk border transitions.
"tech_tree_layout": TechTreeLayout, Only valid during the Month of April. Forced off otherwise."""
"min_tech_cost": MinTechCost,
"max_tech_cost": MaxTechCost,
"tech_cost_distribution": TechCostDistribution,
"tech_cost_mix": TechCostMix,
"ramping_tech_costs": RampingTechCosts,
"silo": Silo,
"satellite": Satellite,
"free_samples": FreeSamples,
"tech_tree_information": TechTreeInformation,
"starting_items": FactorioStartItems,
"free_sample_blacklist": FactorioFreeSampleBlacklist,
"free_sample_whitelist": FactorioFreeSampleWhitelist,
"recipe_time": RecipeTime,
"recipe_ingredients": RecipeIngredients,
"recipe_ingredients_offset": RecipeIngredientsOffset,
"imported_blueprints": ImportedBlueprint,
"world_gen": FactorioWorldGen,
"progressive": Progressive,
"teleport_traps": TeleportTrapCount,
"grenade_traps": GrenadeTrapCount,
"cluster_grenade_traps": ClusterGrenadeTrapCount,
"artillery_traps": ArtilleryTrapCount,
"atomic_rocket_traps": AtomicRocketTrapCount,
"attack_traps": AttackTrapCount,
"evolution_traps": EvolutionTrapCount,
"evolution_trap_increase": EvolutionTrapIncrease,
"death_link": DeathLink,
"energy_link": EnergyLink,
"start_inventory_from_pool": StartInventoryPool,
}
# spoilers below. If you spoil it for yourself, please at least don't spoil it for anyone else.
if datetime.datetime.today().month == 4:
class ChunkShuffle(Toggle):
"""Entrance Randomizer."""
display_name = "Chunk Shuffle"
if datetime.datetime.today().day > 1: @dataclass
ChunkShuffle.__doc__ += """ class FactorioOptions(PerGameCommonOptions):
2023 April Fool's option. Shuffles chunk border transitions.""" max_science_pack: MaxSciencePack
factorio_options["chunk_shuffle"] = ChunkShuffle goal: Goal
tech_tree_layout: TechTreeLayout
min_tech_cost: MinTechCost
max_tech_cost: MaxTechCost
tech_cost_distribution: TechCostDistribution
tech_cost_mix: TechCostMix
ramping_tech_costs: RampingTechCosts
silo: Silo
satellite: Satellite
free_samples: FreeSamples
tech_tree_information: TechTreeInformation
starting_items: FactorioStartItems
free_sample_blacklist: FactorioFreeSampleBlacklist
free_sample_whitelist: FactorioFreeSampleWhitelist
recipe_time: RecipeTime
recipe_ingredients: RecipeIngredients
recipe_ingredients_offset: RecipeIngredientsOffset
imported_blueprints: ImportedBlueprint
world_gen: FactorioWorldGen
progressive: Progressive
teleport_traps: TeleportTrapCount
grenade_traps: GrenadeTrapCount
cluster_grenade_traps: ClusterGrenadeTrapCount
artillery_traps: ArtilleryTrapCount
atomic_rocket_traps: AtomicRocketTrapCount
attack_traps: AttackTrapCount
evolution_traps: EvolutionTrapCount
evolution_trap_increase: EvolutionTrapIncrease
death_link: DeathLink
energy_link: EnergyLink
start_inventory_from_pool: StartInventoryPool
chunk_shuffle: ChunkShuffle

View File

@@ -20,10 +20,10 @@ def _sorter(location: "FactorioScienceLocation"):
def get_shapes(factorio_world: "Factorio") -> Dict["FactorioScienceLocation", Set["FactorioScienceLocation"]]: def get_shapes(factorio_world: "Factorio") -> Dict["FactorioScienceLocation", Set["FactorioScienceLocation"]]:
options = factorio_world.options
world = factorio_world.multiworld world = factorio_world.multiworld
player = factorio_world.player
prerequisites: Dict["FactorioScienceLocation", Set["FactorioScienceLocation"]] = {} prerequisites: Dict["FactorioScienceLocation", Set["FactorioScienceLocation"]] = {}
layout = world.tech_tree_layout[player].value layout = options.tech_tree_layout.value
locations: List["FactorioScienceLocation"] = sorted(factorio_world.science_locations, key=lambda loc: loc.name) locations: List["FactorioScienceLocation"] = sorted(factorio_world.science_locations, key=lambda loc: loc.name)
world.random.shuffle(locations) world.random.shuffle(locations)

View File

@@ -11,7 +11,7 @@ from worlds.LauncherComponents import Component, components, Type, launch_subpro
from worlds.generic import Rules from worlds.generic import Rules
from .Locations import location_pools, location_table from .Locations import location_pools, location_table
from .Mod import generate_mod from .Mod import generate_mod
from .Options import factorio_options, MaxSciencePack, Silo, Satellite, TechTreeInformation, Goal, TechCostDistribution from .Options import FactorioOptions, MaxSciencePack, Silo, Satellite, TechTreeInformation, Goal, TechCostDistribution, TechCostMix
from .Shapes import get_shapes from .Shapes import get_shapes
from .Technologies import base_tech_table, recipe_sources, base_technology_table, \ from .Technologies import base_tech_table, recipe_sources, base_technology_table, \
all_ingredient_names, all_product_sources, required_technologies, get_rocket_requirements, \ all_ingredient_names, all_product_sources, required_technologies, get_rocket_requirements, \
@@ -88,6 +88,9 @@ class Factorio(World):
location_pool: typing.List[FactorioScienceLocation] location_pool: typing.List[FactorioScienceLocation]
advancement_technologies: typing.Set[str] advancement_technologies: typing.Set[str]
options = FactorioOptions
options_dataclass = FactorioOptions
web = FactorioWeb() web = FactorioWeb()
item_name_to_id = all_items item_name_to_id = all_items
@@ -117,11 +120,11 @@ class Factorio(World):
def generate_early(self) -> None: def generate_early(self) -> None:
# if max < min, then swap max and min # if max < min, then swap max and min
if self.multiworld.max_tech_cost[self.player] < self.multiworld.min_tech_cost[self.player]: if self.options.max_tech_cost < self.options.min_tech_cost:
self.multiworld.min_tech_cost[self.player].value, self.multiworld.max_tech_cost[self.player].value = \ self.options.min_tech_cost.value, self.options.max_tech_cost.value = \
self.multiworld.max_tech_cost[self.player].value, self.multiworld.min_tech_cost[self.player].value self.options.max_tech_cost.value, self.options.min_tech_cost.value
self.tech_mix = self.multiworld.tech_cost_mix[self.player] self.tech_mix = self.options.tech_cost_mix
self.skip_silo = self.multiworld.silo[self.player].value == Silo.option_spawn self.skip_silo = self.options.silo.value == Silo.option_spawn
def create_regions(self): def create_regions(self):
player = self.player player = self.player
@@ -132,17 +135,17 @@ class Factorio(World):
nauvis = Region("Nauvis", player, self.multiworld) nauvis = Region("Nauvis", player, self.multiworld)
location_count = len(base_tech_table) - len(useless_technologies) - self.skip_silo + \ location_count = len(base_tech_table) - len(useless_technologies) - self.skip_silo + \
self.multiworld.evolution_traps[player] + \ self.options.evolution_traps + \
self.multiworld.attack_traps[player] + \ self.options.attack_traps + \
self.multiworld.teleport_traps[player] + \ self.options.teleport_traps + \
self.multiworld.grenade_traps[player] + \ self.options.grenade_traps + \
self.multiworld.cluster_grenade_traps[player] + \ self.options.cluster_grenade_traps + \
self.multiworld.atomic_rocket_traps[player] + \ self.options.atomic_rocket_traps + \
self.multiworld.artillery_traps[player] self.options.artillery_traps
location_pool = [] location_pool = []
for pack in sorted(self.multiworld.max_science_pack[self.player].get_allowed_packs()): for pack in sorted(self.options.max_science_pack.get_allowed_packs()):
location_pool.extend(location_pools[pack]) location_pool.extend(location_pools[pack])
try: try:
location_names = self.multiworld.random.sample(location_pool, location_count) location_names = self.multiworld.random.sample(location_pool, location_count)
@@ -151,11 +154,11 @@ class Factorio(World):
raise Exception("Too many traps for too few locations. Either decrease the trap count, " raise Exception("Too many traps for too few locations. Either decrease the trap count, "
f"or increase the location count (higher max science pack). (Player {self.player})") from e f"or increase the location count (higher max science pack). (Player {self.player})") from e
self.science_locations = [FactorioScienceLocation(player, loc_name, self.location_name_to_id[loc_name], nauvis) self.science_locations = [FactorioScienceLocation(player, loc_name, self.location_name_to_id[loc_name], nauvis, self.options.tech_cost_mix)
for loc_name in location_names] for loc_name in location_names]
distribution: TechCostDistribution = self.multiworld.tech_cost_distribution[self.player] distribution: TechCostDistribution = self.options.tech_cost_distribution
min_cost = self.multiworld.min_tech_cost[self.player] min_cost = self.options.min_tech_cost
max_cost = self.multiworld.max_tech_cost[self.player] max_cost = self.options.max_tech_cost
if distribution == distribution.option_even: if distribution == distribution.option_even:
rand_values = (random.randint(min_cost, max_cost) for _ in self.science_locations) rand_values = (random.randint(min_cost, max_cost) for _ in self.science_locations)
else: else:
@@ -164,7 +167,7 @@ class Factorio(World):
distribution.option_high: max_cost}[distribution.value] distribution.option_high: max_cost}[distribution.value]
rand_values = (random.triangular(min_cost, max_cost, mode) for _ in self.science_locations) rand_values = (random.triangular(min_cost, max_cost, mode) for _ in self.science_locations)
rand_values = sorted(rand_values) rand_values = sorted(rand_values)
if self.multiworld.ramping_tech_costs[self.player]: if self.options.ramping_tech_costs:
def sorter(loc: FactorioScienceLocation): def sorter(loc: FactorioScienceLocation):
return loc.complexity, loc.rel_cost return loc.complexity, loc.rel_cost
else: else:
@@ -179,7 +182,7 @@ class Factorio(World):
event = FactorioItem("Victory", ItemClassification.progression, None, player) event = FactorioItem("Victory", ItemClassification.progression, None, player)
location.place_locked_item(event) location.place_locked_item(event)
for ingredient in sorted(self.multiworld.max_science_pack[self.player].get_allowed_packs()): for ingredient in sorted(self.options.max_science_pack.get_allowed_packs()):
location = FactorioLocation(player, f"Automate {ingredient}", None, nauvis) location = FactorioLocation(player, f"Automate {ingredient}", None, nauvis)
nauvis.locations.append(location) nauvis.locations.append(location)
event = FactorioItem(f"Automated {ingredient}", ItemClassification.progression, None, player) event = FactorioItem(f"Automated {ingredient}", ItemClassification.progression, None, player)
@@ -195,10 +198,10 @@ class Factorio(World):
traps = ("Evolution", "Attack", "Teleport", "Grenade", "Cluster Grenade", "Artillery", "Atomic Rocket") traps = ("Evolution", "Attack", "Teleport", "Grenade", "Cluster Grenade", "Artillery", "Atomic Rocket")
for trap_name in traps: for trap_name in traps:
self.multiworld.itempool.extend(self.create_item(f"{trap_name} Trap") for _ in self.multiworld.itempool.extend(self.create_item(f"{trap_name} Trap") for _ in
range(getattr(self.multiworld, range(getattr(self.options,
f"{trap_name.lower().replace(' ', '_')}_traps")[player])) f"{trap_name.lower().replace(' ', '_')}_traps")))
want_progressives = collections.defaultdict(lambda: self.multiworld.progressive[player]. want_progressives = collections.defaultdict(lambda: self.options.progressive.
want_progressives(self.multiworld.random)) want_progressives(self.multiworld.random))
cost_sorted_locations = sorted(self.science_locations, key=lambda location: location.name) cost_sorted_locations = sorted(self.science_locations, key=lambda location: location.name)
@@ -206,7 +209,7 @@ class Factorio(World):
"logistics": 1, "logistics": 1,
"rocket-silo": -1} "rocket-silo": -1}
loc: FactorioScienceLocation loc: FactorioScienceLocation
if self.multiworld.tech_tree_information[player] == TechTreeInformation.option_full: if self.options.tech_tree_information == TechTreeInformation.option_full:
# mark all locations as pre-hinted # mark all locations as pre-hinted
for loc in self.science_locations: for loc in self.science_locations:
loc.revealed = True loc.revealed = True
@@ -237,10 +240,10 @@ class Factorio(World):
player = self.player player = self.player
shapes = get_shapes(self) shapes = get_shapes(self)
for ingredient in self.multiworld.max_science_pack[self.player].get_allowed_packs(): for ingredient in self.options.max_science_pack.get_allowed_packs():
location = world.get_location(f"Automate {ingredient}", player) location = world.get_location(f"Automate {ingredient}", player)
if self.multiworld.recipe_ingredients[self.player]: if self.options.recipe_ingredients:
custom_recipe = self.custom_recipes[ingredient] custom_recipe = self.custom_recipes[ingredient]
location.access_rule = lambda state, ingredient=ingredient, custom_recipe=custom_recipe: \ location.access_rule = lambda state, ingredient=ingredient, custom_recipe=custom_recipe: \
@@ -261,16 +264,16 @@ class Factorio(World):
prerequisites: all(state.can_reach(loc) for loc in locations)) prerequisites: all(state.can_reach(loc) for loc in locations))
silo_recipe = None silo_recipe = None
if self.multiworld.silo[self.player] == Silo.option_spawn: if self.options.silo == Silo.option_spawn:
silo_recipe = self.custom_recipes["rocket-silo"] if "rocket-silo" in self.custom_recipes \ silo_recipe = self.custom_recipes["rocket-silo"] if "rocket-silo" in self.custom_recipes \
else next(iter(all_product_sources.get("rocket-silo"))) else next(iter(all_product_sources.get("rocket-silo")))
part_recipe = self.custom_recipes["rocket-part"] part_recipe = self.custom_recipes["rocket-part"]
satellite_recipe = None satellite_recipe = None
if self.multiworld.goal[self.player] == Goal.option_satellite: if self.options.goal == Goal.option_satellite:
satellite_recipe = self.custom_recipes["satellite"] if "satellite" in self.custom_recipes \ satellite_recipe = self.custom_recipes["satellite"] if "satellite" in self.custom_recipes \
else next(iter(all_product_sources.get("satellite"))) else next(iter(all_product_sources.get("satellite")))
victory_tech_names = get_rocket_requirements(silo_recipe, part_recipe, satellite_recipe) victory_tech_names = get_rocket_requirements(silo_recipe, part_recipe, satellite_recipe)
if self.multiworld.silo[self.player] != Silo.option_spawn: if self.options.silo != Silo.option_spawn:
victory_tech_names.add("rocket-silo") victory_tech_names.add("rocket-silo")
world.get_location("Rocket Launch", player).access_rule = lambda state: all(state.has(technology, player) world.get_location("Rocket Launch", player).access_rule = lambda state: all(state.has(technology, player)
for technology in for technology in
@@ -279,12 +282,12 @@ class Factorio(World):
world.completion_condition[player] = lambda state: state.has('Victory', player) world.completion_condition[player] = lambda state: state.has('Victory', player)
def generate_basic(self): def generate_basic(self):
map_basic_settings = self.multiworld.world_gen[self.player].value["basic"] map_basic_settings = self.options.world_gen.value["basic"]
if map_basic_settings.get("seed", None) is None: # allow seed 0 if map_basic_settings.get("seed", None) is None: # allow seed 0
# 32 bit uint # 32 bit uint
map_basic_settings["seed"] = self.multiworld.per_slot_randoms[self.player].randint(0, 2 ** 32 - 1) map_basic_settings["seed"] = self.multiworld.per_slot_randoms[self.player].randint(0, 2 ** 32 - 1)
start_location_hints: typing.Set[str] = self.multiworld.start_location_hints[self.player].value start_location_hints: typing.Set[str] = self.options.start_location_hints.value
for loc in self.science_locations: for loc in self.science_locations:
# show start_location_hints ingame # show start_location_hints ingame
@@ -308,8 +311,6 @@ class Factorio(World):
return super(Factorio, self).collect_item(state, item, remove) return super(Factorio, self).collect_item(state, item, remove)
option_definitions = factorio_options
@classmethod @classmethod
def stage_write_spoiler(cls, world, spoiler_handle): def stage_write_spoiler(cls, world, spoiler_handle):
factorio_players = world.get_game_players(cls.game) factorio_players = world.get_game_players(cls.game)
@@ -437,25 +438,25 @@ class Factorio(World):
def set_custom_technologies(self): def set_custom_technologies(self):
custom_technologies = {} custom_technologies = {}
allowed_packs = self.multiworld.max_science_pack[self.player].get_allowed_packs() allowed_packs = self.options.max_science_pack.get_allowed_packs()
for technology_name, technology in base_technology_table.items(): for technology_name, technology in base_technology_table.items():
custom_technologies[technology_name] = technology.get_custom(self.multiworld, allowed_packs, self.player) custom_technologies[technology_name] = technology.get_custom(self.multiworld, allowed_packs, self.player)
return custom_technologies return custom_technologies
def set_custom_recipes(self): def set_custom_recipes(self):
ingredients_offset = self.multiworld.recipe_ingredients_offset[self.player] ingredients_offset = self.options.recipe_ingredients_offset
original_rocket_part = recipes["rocket-part"] original_rocket_part = recipes["rocket-part"]
science_pack_pools = get_science_pack_pools() science_pack_pools = get_science_pack_pools()
valid_pool = sorted(science_pack_pools[self.multiworld.max_science_pack[self.player].get_max_pack()] & valid_ingredients) valid_pool = sorted(science_pack_pools[self.options.max_science_pack.get_max_pack()] & valid_ingredients)
self.multiworld.random.shuffle(valid_pool) self.multiworld.random.shuffle(valid_pool)
self.custom_recipes = {"rocket-part": Recipe("rocket-part", original_rocket_part.category, self.custom_recipes = {"rocket-part": Recipe("rocket-part", original_rocket_part.category,
{valid_pool[x]: 10 for x in range(3 + ingredients_offset)}, {valid_pool[x]: 10 for x in range(3 + ingredients_offset)},
original_rocket_part.products, original_rocket_part.products,
original_rocket_part.energy)} original_rocket_part.energy)}
if self.multiworld.recipe_ingredients[self.player]: if self.options.recipe_ingredients:
valid_pool = [] valid_pool = []
for pack in self.multiworld.max_science_pack[self.player].get_ordered_science_packs(): for pack in self.options.max_science_pack.get_ordered_science_packs():
valid_pool += sorted(science_pack_pools[pack]) valid_pool += sorted(science_pack_pools[pack])
self.multiworld.random.shuffle(valid_pool) self.multiworld.random.shuffle(valid_pool)
if pack in recipes: # skips over space science pack if pack in recipes: # skips over space science pack
@@ -463,23 +464,23 @@ class Factorio(World):
ingredients_offset) ingredients_offset)
self.custom_recipes[pack] = new_recipe self.custom_recipes[pack] = new_recipe
if self.multiworld.silo[self.player].value == Silo.option_randomize_recipe \ if self.options.silo.value == Silo.option_randomize_recipe \
or self.multiworld.satellite[self.player].value == Satellite.option_randomize_recipe: or self.options.satellite.value == Satellite.option_randomize_recipe:
valid_pool = set() valid_pool = set()
for pack in sorted(self.multiworld.max_science_pack[self.player].get_allowed_packs()): for pack in sorted(self.options.max_science_pack.get_allowed_packs()):
valid_pool |= science_pack_pools[pack] valid_pool |= science_pack_pools[pack]
if self.multiworld.silo[self.player].value == Silo.option_randomize_recipe: if self.options.silo.value == Silo.option_randomize_recipe:
new_recipe = self.make_balanced_recipe( new_recipe = self.make_balanced_recipe(
recipes["rocket-silo"], valid_pool, recipes["rocket-silo"], valid_pool,
factor=(self.multiworld.max_science_pack[self.player].value + 1) / 7, factor=(self.options.max_science_pack.value + 1) / 7,
ingredients_offset=ingredients_offset) ingredients_offset=ingredients_offset)
self.custom_recipes["rocket-silo"] = new_recipe self.custom_recipes["rocket-silo"] = new_recipe
if self.multiworld.satellite[self.player].value == Satellite.option_randomize_recipe: if self.options.satellite.value == Satellite.option_randomize_recipe:
new_recipe = self.make_balanced_recipe( new_recipe = self.make_balanced_recipe(
recipes["satellite"], valid_pool, recipes["satellite"], valid_pool,
factor=(self.multiworld.max_science_pack[self.player].value + 1) / 7, factor=(self.options.max_science_pack.value + 1) / 7,
ingredients_offset=ingredients_offset) ingredients_offset=ingredients_offset)
self.custom_recipes["satellite"] = new_recipe self.custom_recipes["satellite"] = new_recipe
bridge = "ap-energy-bridge" bridge = "ap-energy-bridge"
@@ -487,16 +488,16 @@ class Factorio(World):
Recipe(bridge, "crafting", {"replace_1": 1, "replace_2": 1, "replace_3": 1, Recipe(bridge, "crafting", {"replace_1": 1, "replace_2": 1, "replace_3": 1,
"replace_4": 1, "replace_5": 1, "replace_6": 1}, "replace_4": 1, "replace_5": 1, "replace_6": 1},
{bridge: 1}, 10), {bridge: 1}, 10),
sorted(science_pack_pools[self.multiworld.max_science_pack[self.player].get_ordered_science_packs()[0]]), sorted(science_pack_pools[self.options.max_science_pack.get_ordered_science_packs()[0]]),
ingredients_offset=ingredients_offset) ingredients_offset=ingredients_offset)
for ingredient_name in new_recipe.ingredients: for ingredient_name in new_recipe.ingredients:
new_recipe.ingredients[ingredient_name] = self.multiworld.random.randint(50, 500) new_recipe.ingredients[ingredient_name] = self.multiworld.random.randint(50, 500)
self.custom_recipes[bridge] = new_recipe self.custom_recipes[bridge] = new_recipe
needed_recipes = self.multiworld.max_science_pack[self.player].get_allowed_packs() | {"rocket-part"} needed_recipes = self.options.max_science_pack.get_allowed_packs() | {"rocket-part"}
if self.multiworld.silo[self.player] != Silo.option_spawn: if self.options.silo != Silo.option_spawn:
needed_recipes |= {"rocket-silo"} needed_recipes |= {"rocket-silo"}
if self.multiworld.goal[self.player].value == Goal.option_satellite: if self.options.goal.value == Goal.option_satellite:
needed_recipes |= {"satellite"} needed_recipes |= {"satellite"}
for recipe in needed_recipes: for recipe in needed_recipes:
@@ -538,7 +539,7 @@ class FactorioScienceLocation(FactorioLocation):
ingredients: typing.Dict[str, int] ingredients: typing.Dict[str, int]
count: int = 0 count: int = 0
def __init__(self, player: int, name: str, address: int, parent: Region): def __init__(self, player: int, name: str, address: int, parent: Region, tech_cost_mix: TechCostMix):
super(FactorioScienceLocation, self).__init__(player, name, address, parent) super(FactorioScienceLocation, self).__init__(player, name, address, parent)
# "AP-{Complexity}-{Cost}" # "AP-{Complexity}-{Cost}"
self.complexity = int(self.name[3]) - 1 self.complexity = int(self.name[3]) - 1
@@ -546,7 +547,7 @@ class FactorioScienceLocation(FactorioLocation):
self.ingredients = {Factorio.ordered_science_packs[self.complexity]: 1} self.ingredients = {Factorio.ordered_science_packs[self.complexity]: 1}
for complexity in range(self.complexity): for complexity in range(self.complexity):
if parent.multiworld.tech_cost_mix[self.player] > parent.multiworld.random.randint(0, 99): if tech_cost_mix > parent.multiworld.random.randint(0, 99):
self.ingredients[Factorio.ordered_science_packs[complexity]] = 1 self.ingredients[Factorio.ordered_science_packs[complexity]] = 1
@property @property

View File

@@ -74,7 +74,6 @@ class FF1World(World):
items = get_options(self.multiworld, 'items', self.player) items = get_options(self.multiworld, 'items', self.player)
goal_rule = generate_rule([[name for name in items.keys() if name in FF1_PROGRESSION_LIST and name != "Shard"]], goal_rule = generate_rule([[name for name in items.keys() if name in FF1_PROGRESSION_LIST and name != "Shard"]],
self.player) self.player)
terminated_event.access_rule = goal_rule
if "Shard" in items.keys(): if "Shard" in items.keys():
def goal_rule_and_shards(state): def goal_rule_and_shards(state):
return goal_rule(state) and state.has("Shard", self.player, 32) return goal_rule(state) and state.has("Shard", self.player, 32)

View File

@@ -1,5 +1,4 @@
import collections import collections
import logging
import typing import typing
from BaseClasses import LocationProgressType, MultiWorld, Location, Region, Entrance from BaseClasses import LocationProgressType, MultiWorld, Location, Region, Entrance
@@ -16,9 +15,9 @@ else:
def locality_needed(world: MultiWorld) -> bool: def locality_needed(world: MultiWorld) -> bool:
for player in world.player_ids: for player in world.player_ids:
if world.local_items[player].value: if world.worlds[player].options.local_items.value:
return True return True
if world.non_local_items[player].value: if world.worlds[player].options.non_local_items.value:
return True return True
# Group # Group
@@ -41,12 +40,12 @@ def locality_rules(world: MultiWorld):
forbid_data[sender][receiver].update(items) forbid_data[sender][receiver].update(items)
for receiving_player in world.player_ids: for receiving_player in world.player_ids:
local_items: typing.Set[str] = world.local_items[receiving_player].value local_items: typing.Set[str] = world.worlds[receiving_player].options.local_items.value
if local_items: if local_items:
for sending_player in world.player_ids: for sending_player in world.player_ids:
if receiving_player != sending_player: if receiving_player != sending_player:
forbid(sending_player, receiving_player, local_items) forbid(sending_player, receiving_player, local_items)
non_local_items: typing.Set[str] = world.non_local_items[receiving_player].value non_local_items: typing.Set[str] = world.worlds[receiving_player].options.non_local_items.value
if non_local_items: if non_local_items:
forbid(receiving_player, receiving_player, non_local_items) forbid(receiving_player, receiving_player, non_local_items)
@@ -82,18 +81,15 @@ def locality_rules(world: MultiWorld):
i.name not in sending_blockers[i.player] and old_rule(i) i.name not in sending_blockers[i.player] and old_rule(i)
def exclusion_rules(multiworld: MultiWorld, player: int, exclude_locations: typing.Set[str]) -> None: def exclusion_rules(world: MultiWorld, player: int, exclude_locations: typing.Set[str]) -> None:
for loc_name in exclude_locations: for loc_name in exclude_locations:
try: try:
location = multiworld.get_location(loc_name, player) location = world.get_location(loc_name, player)
except KeyError as e: # failed to find the given location. Check if it's a legitimate location except KeyError as e: # failed to find the given location. Check if it's a legitimate location
if loc_name not in multiworld.worlds[player].location_name_to_id: if loc_name not in world.worlds[player].location_name_to_id:
raise Exception(f"Unable to exclude location {loc_name} in player {player}'s world.") from e raise Exception(f"Unable to exclude location {loc_name} in player {player}'s world.") from e
else: else:
if not location.event: location.progress_type = LocationProgressType.EXCLUDED
location.progress_type = LocationProgressType.EXCLUDED
else:
logging.warning(f"Unable to exclude location {loc_name} in player {player}'s world.")
def set_rule(spot: typing.Union["BaseClasses.Location", "BaseClasses.Entrance"], rule: CollectionRule): def set_rule(spot: typing.Union["BaseClasses.Location", "BaseClasses.Entrance"], rule: CollectionRule):

View File

@@ -444,8 +444,6 @@ def set_rules(hylics2world):
lambda state: paddle(state, player)) lambda state: paddle(state, player))
add_rule(world.get_location("Arcade 1: Alcove Medallion", player), add_rule(world.get_location("Arcade 1: Alcove Medallion", player),
lambda state: paddle(state, player)) lambda state: paddle(state, player))
add_rule(world.get_location("Arcade 1: Lava Medallion", player),
lambda state: paddle(state, player))
add_rule(world.get_location("Foglast: Under Lair Medallion", player), add_rule(world.get_location("Foglast: Under Lair Medallion", player),
lambda state: bridge_key(state, player)) lambda state: bridge_key(state, player))
add_rule(world.get_location("Foglast: Mid-Air Medallion", player), add_rule(world.get_location("Foglast: Mid-Air Medallion", player),

View File

@@ -80,6 +80,11 @@ class KH2Context(CommonContext):
}, },
}, },
} }
self.front_of_inventory = {
"Sora": 0x2546,
"Donald": 0x2658,
"Goofy": 0x276C,
}
self.kh2seedname = None self.kh2seedname = None
self.kh2slotdata = None self.kh2slotdata = None
self.itemamount = {} self.itemamount = {}
@@ -164,14 +169,6 @@ class KH2Context(CommonContext):
self.ability_code_list = None self.ability_code_list = None
self.master_growth = {"High Jump", "Quick Run", "Dodge Roll", "Aerial Dodge", "Glide"} self.master_growth = {"High Jump", "Quick Run", "Dodge Roll", "Aerial Dodge", "Glide"}
self.base_hp = 20
self.base_mp = 100
self.base_drive = 5
self.base_accessory_slots = 1
self.base_armor_slots = 1
self.base_item_slots = 3
self.front_ability_slots = [0x2546, 0x2658, 0x276C, 0x2548, 0x254A, 0x254C, 0x265A, 0x265C, 0x265E, 0x276E, 0x2770, 0x2772]
async def server_auth(self, password_requested: bool = False): async def server_auth(self, password_requested: bool = False):
if password_requested and not self.password: if password_requested and not self.password:
await super(KH2Context, self).server_auth(password_requested) await super(KH2Context, self).server_auth(password_requested)
@@ -222,12 +219,6 @@ class KH2Context(CommonContext):
def kh2_read_byte(self, address): def kh2_read_byte(self, address):
return int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + address, 1), "big") return int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + address, 1), "big")
def kh2_read_int(self, address):
return self.kh2.read_int(self.kh2.base_address + address)
def kh2_write_int(self, address, value):
self.kh2.write_int(self.kh2.base_address + address, value)
def on_package(self, cmd: str, args: dict): def on_package(self, cmd: str, args: dict):
if cmd in {"RoomInfo"}: if cmd in {"RoomInfo"}:
self.kh2seedname = args['seed_name'] self.kh2seedname = args['seed_name']
@@ -485,7 +476,7 @@ class KH2Context(CommonContext):
async def give_item(self, item, location): async def give_item(self, item, location):
try: try:
# todo: ripout all the itemtype stuff and just have one dictionary. the only thing that needs to be tracked from the server/local is abilites # todo: ripout all the itemtype stuff and just have one dictionary. the only thing that needs to be tracked from the server/local is abilites
itemname = self.lookup_id_to_item[item] itemname = self.lookup_id_to_item[item]
itemdata = self.item_name_to_data[itemname] itemdata = self.item_name_to_data[itemname]
# itemcode = self.kh2_item_name_to_id[itemname] # itemcode = self.kh2_item_name_to_id[itemname]
@@ -516,8 +507,6 @@ class KH2Context(CommonContext):
ability_slot = self.kh2_seed_save_cache["GoofyInvo"][1] ability_slot = self.kh2_seed_save_cache["GoofyInvo"][1]
self.kh2_seed_save_cache["AmountInvo"]["Ability"][itemname].append(ability_slot) self.kh2_seed_save_cache["AmountInvo"]["Ability"][itemname].append(ability_slot)
self.kh2_seed_save_cache["GoofyInvo"][1] -= 2 self.kh2_seed_save_cache["GoofyInvo"][1] -= 2
if ability_slot in self.front_ability_slots:
self.front_ability_slots.remove(ability_slot)
elif len(self.kh2_seed_save_cache["AmountInvo"]["Ability"][itemname]) < \ elif len(self.kh2_seed_save_cache["AmountInvo"]["Ability"][itemname]) < \
self.AbilityQuantityDict[itemname]: self.AbilityQuantityDict[itemname]:
@@ -529,14 +518,11 @@ class KH2Context(CommonContext):
ability_slot = self.kh2_seed_save_cache["DonaldInvo"][0] ability_slot = self.kh2_seed_save_cache["DonaldInvo"][0]
self.kh2_seed_save_cache["AmountInvo"]["Ability"][itemname].append(ability_slot) self.kh2_seed_save_cache["AmountInvo"]["Ability"][itemname].append(ability_slot)
self.kh2_seed_save_cache["DonaldInvo"][0] -= 2 self.kh2_seed_save_cache["DonaldInvo"][0] -= 2
else: elif itemname in self.goofy_ability_set:
ability_slot = self.kh2_seed_save_cache["GoofyInvo"][0] ability_slot = self.kh2_seed_save_cache["GoofyInvo"][0]
self.kh2_seed_save_cache["AmountInvo"]["Ability"][itemname].append(ability_slot) self.kh2_seed_save_cache["AmountInvo"]["Ability"][itemname].append(ability_slot)
self.kh2_seed_save_cache["GoofyInvo"][0] -= 2 self.kh2_seed_save_cache["GoofyInvo"][0] -= 2
if ability_slot in self.front_ability_slots:
self.front_ability_slots.remove(ability_slot)
elif itemdata.memaddr in {0x36C4, 0x36C5, 0x36C6, 0x36C0, 0x36CA}: elif itemdata.memaddr in {0x36C4, 0x36C5, 0x36C6, 0x36C0, 0x36CA}:
# if memaddr is in a bitmask location in memory # if memaddr is in a bitmask location in memory
if itemname not in self.kh2_seed_save_cache["AmountInvo"]["Bitmask"]: if itemname not in self.kh2_seed_save_cache["AmountInvo"]["Bitmask"]:
@@ -629,7 +615,7 @@ class KH2Context(CommonContext):
master_sell = master_equipment | master_staff | master_shield master_sell = master_equipment | master_staff | master_shield
await asyncio.create_task(self.IsInShop(master_sell)) await asyncio.create_task(self.IsInShop(master_sell))
# print(self.kh2_seed_save_cache["AmountInvo"]["Ability"])
for item_name in master_amount: for item_name in master_amount:
item_data = self.item_name_to_data[item_name] item_data = self.item_name_to_data[item_name]
amount_of_items = 0 amount_of_items = 0
@@ -687,10 +673,10 @@ class KH2Context(CommonContext):
self.kh2_write_short(self.Save + slot, item_data.memaddr) self.kh2_write_short(self.Save + slot, item_data.memaddr)
# removes the duped ability if client gave faster than the game. # removes the duped ability if client gave faster than the game.
for ability in self.front_ability_slots: for charInvo in {"Sora", "Donald", "Goofy"}:
if self.kh2_read_short(self.Save + ability) != 0: if self.kh2_read_short(self.Save + self.front_of_inventory[charInvo]) != 0:
print(f"removed {self.Save + ability} from {ability}") print(f"removed {self.Save + self.front_of_inventory[charInvo]} from {charInvo}")
self.kh2_write_short(self.Save + ability, 0) self.kh2_write_short(self.Save + self.front_of_inventory[charInvo], 0)
# remove the dummy level 1 growths if they are in these invo slots. # remove the dummy level 1 growths if they are in these invo slots.
for inventorySlot in {0x25CE, 0x25D0, 0x25D2, 0x25D4, 0x25D6, 0x25D8}: for inventorySlot in {0x25CE, 0x25D0, 0x25D2, 0x25D4, 0x25D6, 0x25D8}:
@@ -754,60 +740,15 @@ class KH2Context(CommonContext):
self.kh2_write_byte(self.Save + item_data.memaddr, amount_of_items) self.kh2_write_byte(self.Save + item_data.memaddr, amount_of_items)
for item_name in master_stat: for item_name in master_stat:
item_data = self.item_name_to_data[item_name]
amount_of_items = 0 amount_of_items = 0
amount_of_items += self.kh2_seed_save_cache["AmountInvo"]["StatIncrease"][item_name] amount_of_items += self.kh2_seed_save_cache["AmountInvo"]["StatIncrease"][item_name]
if self.kh2_read_byte(self.Slot1 + 0x1B2) >= 5:
if item_name == ItemName.MaxHPUp:
if self.kh2_read_byte(self.Save + 0x2498) < 3: # Non-Critical
Bonus = 5
else: # Critical
Bonus = 2
if self.kh2_read_int(self.Slot1 + 0x004) != self.base_hp + (Bonus * amount_of_items):
self.kh2_write_int(self.Slot1 + 0x004, self.base_hp + (Bonus * amount_of_items))
elif item_name == ItemName.MaxMPUp:
if self.kh2_read_byte(self.Save + 0x2498) < 3: # Non-Critical
Bonus = 10
else: # Critical
Bonus = 5
if self.kh2_read_int(self.Slot1 + 0x184) != self.base_mp + (Bonus * amount_of_items):
self.kh2_write_int(self.Slot1 + 0x184, self.base_mp + (Bonus * amount_of_items))
elif item_name == ItemName.DriveGaugeUp:
current_max_drive = self.kh2_read_byte(self.Slot1 + 0x1B2)
# change when max drive is changed from 6 to 4
if current_max_drive < 9 and current_max_drive != self.base_drive + amount_of_items:
self.kh2_write_byte(self.Slot1 + 0x1B2, self.base_drive + amount_of_items)
elif item_name == ItemName.AccessorySlotUp:
current_accessory = self.kh2_read_byte(self.Save + 0x2501)
if current_accessory != self.base_accessory_slots + amount_of_items:
if 4 > current_accessory < self.base_accessory_slots + amount_of_items:
self.kh2_write_byte(self.Save + 0x2501, current_accessory + 1)
elif self.base_accessory_slots + amount_of_items < 4:
self.kh2_write_byte(self.Save + 0x2501, self.base_accessory_slots + amount_of_items)
elif item_name == ItemName.ArmorSlotUp:
current_armor_slots = self.kh2_read_byte(self.Save + 0x2500)
if current_armor_slots != self.base_armor_slots + amount_of_items:
if 4 > current_armor_slots < self.base_armor_slots + amount_of_items:
self.kh2_write_byte(self.Save + 0x2500, current_armor_slots + 1)
elif self.base_armor_slots + amount_of_items < 4:
self.kh2_write_byte(self.Save + 0x2500, self.base_armor_slots + amount_of_items)
elif item_name == ItemName.ItemSlotUp:
current_item_slots = self.kh2_read_byte(self.Save + 0x2502)
if current_item_slots != self.base_item_slots + amount_of_items:
if 8 > current_item_slots < self.base_item_slots + amount_of_items:
self.kh2_write_byte(self.Save + 0x2502, current_item_slots + 1)
elif self.base_item_slots + amount_of_items < 8:
self.kh2_write_byte(self.Save + 0x2502, self.base_item_slots + amount_of_items)
# if self.kh2_read_byte(self.Save + item_data.memaddr) != amount_of_items \
# and self.kh2_read_byte(self.Slot1 + 0x1B2) >= 5 and \
# self.kh2_read_byte(self.Save + 0x23DF) & 0x1 << 3 > 0 and self.kh2_read_byte(0x741320) in {10, 8}:
# self.kh2_write_byte(self.Save + item_data.memaddr, amount_of_items)
# if slot1 has 5 drive gauge and goa lost illusion is checked and they are not in a cutscene
if self.kh2_read_byte(self.Save + item_data.memaddr) != amount_of_items \
and self.kh2_read_byte(self.Slot1 + 0x1B2) >= 5 and \
self.kh2_read_byte(self.Save + 0x23DF) & 0x1 << 3 > 0 and self.kh2_read_byte(0x741320) in {10, 8}:
self.kh2_write_byte(self.Save + item_data.memaddr, amount_of_items)
if "PoptrackerVersionCheck" in self.kh2slotdata: if "PoptrackerVersionCheck" in self.kh2slotdata:
if self.kh2slotdata["PoptrackerVersionCheck"] > 4.2 and self.kh2_read_byte(self.Save + 0x3607) != 1: # telling the goa they are on version 4.3 if self.kh2slotdata["PoptrackerVersionCheck"] > 4.2 and self.kh2_read_byte(self.Save + 0x3607) != 1: # telling the goa they are on version 4.3
self.kh2_write_byte(self.Save + 0x3607, 1) self.kh2_write_byte(self.Save + 0x3607, 1)

View File

@@ -268,6 +268,7 @@ class KH2WorldRules(KH2Rules):
add_item_rule(location, lambda item: item.player == self.player and item.name in DonaldAbility_Table.keys()) add_item_rule(location, lambda item: item.player == self.player and item.name in DonaldAbility_Table.keys())
def set_kh2_goal(self): def set_kh2_goal(self):
final_xemnas_location = self.multiworld.get_location(LocationName.FinalXemnas, self.player) final_xemnas_location = self.multiworld.get_location(LocationName.FinalXemnas, self.player)
if self.multiworld.Goal[self.player] == "three_proofs": if self.multiworld.Goal[self.player] == "three_proofs":
final_xemnas_location.access_rule = lambda state: self.kh2_has_all(three_proofs, state) final_xemnas_location.access_rule = lambda state: self.kh2_has_all(three_proofs, state)
@@ -290,8 +291,8 @@ class KH2WorldRules(KH2Rules):
else: else:
self.multiworld.completion_condition[self.player] = lambda state: state.has(ItemName.Bounty, self.player, self.multiworld.BountyRequired[self.player].value) self.multiworld.completion_condition[self.player] = lambda state: state.has(ItemName.Bounty, self.player, self.multiworld.BountyRequired[self.player].value)
else: else:
final_xemnas_location.access_rule = lambda state: state.has(ItemName.Bounty, self.player, self.multiworld.BountyRequired[self.player].value) and \ final_xemnas_location.access_rule = lambda state: state.has(ItemName.Bounty, self.player, self.multiworld.BountyRequired[self.player].value) and\
state.has(ItemName.LuckyEmblem, self.player, self.multiworld.LuckyEmblemsRequired[self.player].value) state.has(ItemName.LuckyEmblem, self.player, self.multiworld.LuckyEmblemsRequired[self.player].value)
if self.multiworld.FinalXemnas[self.player]: if self.multiworld.FinalXemnas[self.player]:
self.multiworld.completion_condition[self.player] = lambda state: state.has(ItemName.Victory, self.player, 1) self.multiworld.completion_condition[self.player] = lambda state: state.has(ItemName.Victory, self.player, 1)
else: else:

View File

@@ -35,10 +35,6 @@ class NoitaWorld(World):
web = NoitaWeb() web = NoitaWeb()
def generate_early(self):
if not self.multiworld.get_player_name(self.player).isascii():
raise Exception("Noita yaml's slot name has invalid character(s).")
# Returned items will be sent over to the client # Returned items will be sent over to the client
def fill_slot_data(self): def fill_slot_data(self):
return {name: getattr(self.multiworld, name)[self.player].value for name in self.option_definitions} return {name: getattr(self.multiworld, name)[self.player].value for name in self.option_definitions}

View File

@@ -40,8 +40,6 @@ or try restarting your game.
### What is a YAML and why do I need one? ### What is a YAML and why do I need one?
You can see the [basic multiworld setup guide](/tutorial/Archipelago/setup/en) here on the Archipelago website to learn You can see the [basic multiworld setup guide](/tutorial/Archipelago/setup/en) here on the Archipelago website to learn
about why Archipelago uses YAML files and what they're for. about why Archipelago uses YAML files and what they're for.
Please note that Noita only allows you to type certain characters for your slot name.
These characters are: `` !#$%&'()+,-.0123456789;=@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmnopqrstuvwxyz{}~<>|\/``
### Where do I get a YAML? ### Where do I get a YAML?
You can use the [game settings page for Noita](/games/Noita/player-settings) here on the Archipelago website to You can use the [game settings page for Noita](/games/Noita/player-settings) here on the Archipelago website to
@@ -56,4 +54,4 @@ Place the unzipped pack in the `packs` folder. Then, open Poptracker and open th
Click on the "AP" symbol at the top, then enter the desired address, slot name, and password. Click on the "AP" symbol at the top, then enter the desired address, slot name, and password.
That's all you need for it. It will provide you with a quick reference to see which checks you've done and That's all you need for it. It will provide you with a quick reference to see which checks you've done and
which checks you still have left. which checks you still have left.

View File

@@ -1,70 +1,422 @@
# Guide de configuration pour Ocarina of Time Archipelago # Guide d'installation Archipelago pour Ocarina of Time
## Important ## Important
Comme nous utilisons BizHawk, ce guide s'applique uniquement aux systèmes Windows et Linux. Comme nous utilisons BizHawk, ce guide ne s'applique qu'aux systèmes Windows et Linux.
## Logiciel requis ## Logiciel requis
- BizHawk : [Sorties BizHawk de TASVideos](https://tasvideos.org/BizHawk/ReleaseHistory) - BizHawk : [BizHawk sort de TASVideos] (https://tasvideos.org/BizHawk/ReleaseHistory)
- Les versions 2.3.1 et ultérieures sont prises en charge. La version 2.7 est recommandée pour des raisons de stabilité. - Les versions 2.3.1 et ultérieures sont prises en charge. La version 2.7 est recommandée pour la stabilité.
- Des instructions d'installation détaillées pour BizHawk peuvent être trouvées sur le lien ci-dessus. - Des instructions d'installation détaillées pour BizHawk peuvent être trouvées sur le lien ci-dessus.
- Les utilisateurs Windows doivent d'abord exécuter le programme d'installation des prérequis, qui peut également être trouvé sur le lien ci-dessus. - Les utilisateurs Windows doivent d'abord exécuter le programme d'installation prereq, qui peut également être trouvé sur le lien ci-dessus.
- Le client Archipelago intégré, qui peut être installé [ici](https://github.com/ArchipelagoMW/Archipelago/releases) - Le client Archipelago intégré, qui peut être installé [ici](https://github.com/ArchipelagoMW/Archipelago/releases)
(sélectionnez « Ocarina of Time Client » lors de l'installation). (sélectionnez `Ocarina of Time Client` lors de l'installation).
- Une ROM Ocarina of Time v1.0. - Une ROM Ocarina of Time v1.0.
## Configuration de BizHawk ## Configuration de BizHawk
Une fois BizHawk installé, ouvrez EmuHawk et modifiez les paramètres suivants : Une fois BizHawk installé, ouvrez BizHawk et modifiez les paramètres suivants :
- (≤ 2,8) Allez dans Config > Personnaliser. Passez à l'onglet Avancé, puis faites passer le Lua Core de "NLua+KopiLua" à - Allez dans Config > Personnaliser. Basculez vers l'onglet Avancé, puis basculez le Lua Core de "NLua+KopiLua" vers
"Lua+LuaInterface". Puis redémarrez EmuHawk. Ceci est nécessaire pour que le script Lua fonctionne correctement. "Interface Lua+Lua". Redémarrez ensuite BizHawk. Ceci est nécessaire pour que le script Lua fonctionne correctement.
**REMARQUE : Même si « Lua+LuaInterface » est déjà sélectionné, basculez entre les deux options et resélectionnez-la. Nouvelles installations** **REMARQUE : Même si "Lua+LuaInterface" est déjà sélectionné, basculez entre les deux options et resélectionnez-le. Nouvelles installations**
**des versions plus récentes d'EmuHawk ont tendance à afficher "Lua+LuaInterface" comme option sélectionnée par défaut mais ce pendant refait l'épate juste au dessus par précautions** ** des versions plus récentes de BizHawk ont tendance à afficher "Lua+LuaInterface" comme option sélectionnée par défaut mais se chargent toujours **
- Sous Config > Personnaliser > Avancé, assurez-vous que la case AutoSaveRAM est cochée et cliquez sur le bouton 5s. **"NLua+KopiLua" jusqu'à ce que cette étape soit terminée.**
Cela réduit la possibilité de perdre des données de sauvegarde en cas de crash de l'émulateur. - Sous Config > Personnaliser > Avancé, assurez-vous que la case pour AutoSaveRAM est cochée et cliquez sur le bouton 5s.
- Sous Config > Personnaliser, cochez les cases « Exécuter en arrière-plan » et « Accepter la saisie en arrière-plan ». Cela vous permettra continuez à jouer en arrière-plan, même si une autre fenêtre est sélectionnée. Cela réduit la possibilité de perdre des données de sauvegarde en cas de plantage de l'émulateur.
- Sous Config > Hotkeys, de nombreux raccourcis clavier sont répertoriés, dont beaucoup sont liés aux touches communes du clavier. Vous voudrez probablement pour désactiver la plupart d'entre eux, ce que vous pouvez faire rapidement en utilisant « Esc ». - Sous Config > Personnaliser, cochez les cases "Exécuter en arrière-plan" et "Accepter la saisie en arrière-plan". Cela vous permettra de
- Si vous jouez avec une manette, lorsque vous associez des commandes, désactivez "P1 A Up", "P1 A Down", "P1 A Left" et "P1 A Right". continuer à jouer en arrière-plan, même si une autre fenêtre est sélectionnée.
car ceux-ci interfèrent avec la visée sils sont liés. Définissez plutôt l'entrée directionnelle à l'aide de l'onglet Analogique. - Sous Config> Raccourcis clavier, de nombreux raccourcis clavier sont répertoriés, dont beaucoup sont liés aux touches communes du clavier. Vous voudrez probablement
- Sous N64, activez "Utiliser le connecteur d'extension". Ceci est nécessaire pour que les états de sauvegarde fonctionnent. désactiver la plupart d'entre eux, ce que vous pouvez faire rapidement en utilisant `Esc`.
- Si vous jouez avec une manette, lorsque vous liez les commandes, désactivez "P1 A Up", "P1 A Down", "P1 A Left" et "P1 A Right"
car ceux-ci interfèrent avec la visée s'ils sont liés. Définissez l'entrée directionnelle à l'aide de l'onglet Analogique à la place.
- Sous N64, activez "Utiliser l'emplacement d'extension". Ceci est nécessaire pour que les sauvegardes fonctionnent.
(Le menu N64 n'apparaît qu'après le chargement d'une ROM.) (Le menu N64 n'apparaît qu'après le chargement d'une ROM.)
Il est fortement recommandé d'associer les extensions de rom N64 (\*.n64, \*.z64) à l'EmuHawk que nous venons d'installer. Il est fortement recommandé d'associer les extensions de rom N64 (\*.n64, \*.z64) au BizHawk que nous venons d'installer.
Pour ce faire, vous devez simplement rechercher n'importe quelle rom N64 que vous possédez, faire un clic droit et sélectionner "Ouvrir avec...", déplier la liste qui apparaît et sélectionnez l'option du bas "Rechercher une autre application", puis accédez au dossier BizHawk et sélectionnez EmuHawk.exe. Pour ce faire, nous devons simplement rechercher n'importe quelle rom N64 que nous possédons, faire un clic droit et sélectionner "Ouvrir avec ...", dépliez
la liste qui apparaît et sélectionnez l'option du bas "Rechercher une autre application", puis naviguez jusqu'au dossier BizHawk
et sélectionnez EmuHawk.exe.
Un guide de configuration BizHawk alternatif ainsi que divers conseils de dépannage sont disponibles Un guide de configuration BizHawk alternatif ainsi que divers conseils de dépannage peuvent être trouvés
[ici](https://wiki.ootrandomizer.com/index.php?title=Bizhawk). [ici](https://wiki.ootrandomizer.com/index.php?title=Bizhawk).
## Créer un fichier de configuration (.yaml) ## Configuration de votre fichier YAML
### Qu'est-ce qu'un fichier de configuration et pourquoi en ai-je besoin ? ### Qu'est-ce qu'un fichier YAML et pourquoi en ai-je besoin ?
Consultez le guide sur la configuration d'un YAML de base lors de la configuration de l'archipel. Votre fichier YAML contient un ensemble d'options de configuration qui fournissent au générateur des informations sur la façon dont il doit
guide : [Guide de configuration de base de Multiworld](/tutorial/Archipelago/setup/en) générer votre jeu. Chaque joueur d'un multimonde fournira son propre fichier YAML. Cette configuration permet à chaque joueur de profiter
d'une expérience personnalisée à leur goût, et différents joueurs dans le même multimonde peuvent tous avoir des options différentes.
### Où puis-je obtenir un fichier de configuration (.yaml) ? ### Où puis-je obtenir un fichier YAML ?
La page Paramètres du lecteur sur le site Web vous permet de configurer vos paramètres personnels et d'exporter un fichier de configuration depuis eux. Page des paramètres du joueur : [Page des paramètres du joueur d'Ocarina of Time](/games/Ocarina%20of%20Time/player-settings) Un yaml OoT de base ressemblera à ceci. Il y a beaucoup d'options cosmétiques qui ont été supprimées pour le plaisir de ce
tutoriel, si vous voulez voir une liste complète, téléchargez Archipelago depuis
la [Archipelago Releases Page](https://github.com/ArchipelagoMW/Archipelago/releases) et recherchez l'exemple de fichier dans
le dossier "Lecteurs".
### Vérification de votre fichier de configuration ``` yaml
description: Modèle par défaut d'Ocarina of Time # Utilisé pour décrire votre yaml. Utile si vous avez plusieurs fichiers
# Votre nom dans le jeu. Les espaces seront remplacés par des underscores et il y a une limite de 16 caractères
name: VotreNom
game:
Ocarina of Time: 1
requires:
version: 0.1.7 # Version d'Archipelago requise pour que ce yaml fonctionne comme prévu.
# Options partagées prises en charge par tous les jeux :
accessibility:
items: 0 # Garantit que vous pourrez acquérir tous les articles, mais vous ne pourrez peut-être pas accéder à tous les emplacements
locations: 50 # Garantit que vous pourrez accéder à tous les emplacements, et donc à tous les articles
none: 0 # Garantit seulement que le jeu est battable. Vous ne pourrez peut-être pas accéder à tous les emplacements ou acquérir tous les objets
progression_balancing: # Un système pour réduire le BK, comme dans les périodes où vous ne pouvez rien faire, en déplaçant vos éléments dans une sphère d'accès antérieure
0: 0 # Choisissez un nombre inférieur si cela ne vous dérange pas d'avoir un multimonde plus long, ou si vous pouvez glitch / faire du hors logique.
25: 0
50: 50 # Faites en sorte que vous ayez probablement des choses à faire.
99: 0 # Obtenez les éléments importants tôt et restez en tête de la progression.
Ocarina of Time:
logic_rules: # définit la logique utilisée pour le générateur.
glitchless: 50
glitched: 0
no_logic: 0
logic_no_night_tokens_without_suns_song: # Les skulltulas nocturnes nécessiteront logiquement le Chant du soleil.
false: 50
true: 0
open_forest: # Définissez l'état de la forêt de Kokiri et du chemin vers l'arbre Mojo.
open: 50
closed_deku: 0
closed: 0
open_kakariko: # Définit l'état de la porte du village de Kakariko.
open: 50
zelda: 0
closed: 0
open_door_of_time: # Ouvre la Porte du Temps par défaut, sans le Chant du Temps.
false: 0
true: 50
zora_fountain: # Définit l'état du roi Zora, bloquant le chemin vers la fontaine de Zora.
open: 0
adult: 0
closed: 50
gerudo_fortress: # Définit les conditions d'accès à la forteresse Gerudo.
normal: 0
fast: 50
open: 0
bridge: # Définit les exigences pour le pont arc-en-ciel.
open: 0
vanilla: 0
stones: 0
medallions: 50
dungeons: 0
tokens: 0
trials: # Définit le nombre d'épreuves requises dans le Château de Ganon.
# vous pouvez ajouter des valeurs supplémentaires entre minimum et maximum
0: 50 # valeur minimale
6: 0 # valeur maximale
random: 0
random-low: 0
random-higt: 0
starting_age: # Choisissez l'âge auquel Link commencera.
child: 50
adult: 0
triforce_hunt: # Rassemblez des morceaux de la Triforce dispersés dans le monde entier pour terminer le jeu.
false: 50
true: 0
triforce_goal: # Nombre de pièces Triforce nécessaires pour terminer le jeu. Nombre total placé déterminé par le paramètre Item Pool.
# vous pouvez ajouter des valeurs supplémentaires entre minimum et maximum
1: 0 # valeur minimale
50: 0 # valeur maximale
random: 0
random-low: 0
random-higt: 0
20: 50
bombchus_in_logic: # Les Bombchus sont correctement pris en compte dans la logique. Le premier pack trouvé aura 20 chus ; Kokiri Shop et Bazaar vendent des recharges ; bombchus ouvre Bombchu Bowling.
false: 50
true: 0
bridge_stones: # Définissez le nombre de pierres spirituelles requises pour le pont arc-en-ciel.
# vous pouvez ajouter des valeurs supplémentaires entre minimum et maximum
0: 0 # valeur minimale
3: 50 # valeur maximale
random: 0
random-low: 0
random-high: 0
bridge_medallions: # Définissez le nombre de médaillons requis pour le pont arc-en-ciel.
# vous pouvez ajouter des valeurs supplémentaires entre minimum et maximum
0: 0 # valeur minimale
6: 50 # valeur maximale
random: 0
random-low: 0
random-high: 0
bridge_rewards: # Définissez le nombre de récompenses de donjon requises pour le pont arc-en-ciel.
# vous pouvez ajouter des valeurs supplémentaires entre minimum et maximum
0: 0 # valeur minimale
9: 50 # valeur maximale
random: 0
random-low: 0
random-high: 0
bridge_tokens: # Définissez le nombre de jetons Gold Skulltula requis pour le pont arc-en-ciel.
# vous pouvez ajouter des valeurs supplémentaires entre minimum et maximum
0: 0 # valeur minimale
100: 50 # valeur maximale
random: 0
random-low: 0
random-high: 0
shuffle_mapcompass: # Contrôle où mélanger les cartes et boussoles des donjons.
remove: 0
startwith: 50
vanilla: 0
dungeon: 0
overworld: 0
any_dungeon: 0
keysanity: 0
shuffle_smallkeys: # Contrôle où mélanger les petites clés de donjon.
remove: 0
vanilla: 0
dungeon: 50
overworld: 0
any_dungeon: 0
keysanity: 0
shuffle_hideoutkeys: # Contrôle où mélanger les petites clés de la Forteresse Gerudo.
vanilla: 50
overworld: 0
any_dungeon: 0
keysanity: 0
shuffle_bosskeys: # Contrôle où mélanger les clés du boss, à l'exception de la clé du boss du château de Ganon.
remove: 0
vanilla: 0
dungeon: 50
overworld: 0
any_dungeon: 0
keysanity: 0
shuffle_ganon_bosskey: # Contrôle où mélanger la clé du patron du château de Ganon.
remove: 50
vanilla: 0
dungeon: 0
overworld: 0
any_dungeon: 0
keysanity: 0
on_lacs: 0
enhance_map_compass: # La carte indique si un donjon est vanille ou MQ. La boussole indique quelle est la récompense du donjon.
false: 50
true: 0
lacs_condition: # Définissez les exigences pour la cinématique de la Flèche lumineuse dans le Temple du temps.
vanilla: 50
stones: 0
medallions: 0
dungeons: 0
tokens: 0
lacs_stones: # Définissez le nombre de pierres spirituelles requises pour le LACS.
# vous pouvez ajouter des valeurs supplémentaires entre minimum et maximum
0: 0 # valeur minimale
3: 50 # valeur maximale
random: 0
random-low: 0
random-high: 0
lacs_medallions: # Définissez le nombre de médaillons requis pour LACS.
# vous pouvez ajouter des valeurs supplémentaires entre minimum et maximum
0: 0 # valeur minimale
6: 50 # valeur maximale
random: 0
random-low: 0
random-high: 0
lacs_rewards: # Définissez le nombre de récompenses de donjon requises pour LACS.
# vous pouvez ajouter des valeurs supplémentaires entre minimum et maximum
0: 0 # valeur minimale
9: 50 # valeur maximale
random: 0
random-low: 0
random-high: 0
lacs_tokens: # Définissez le nombre de jetons Gold Skulltula requis pour le LACS.
# vous pouvez ajouter des valeurs supplémentaires entre minimum et maximum
0: 0 # valeur minimale
100: 50 # valeur maximale
random: 0
random-low: 0
random-high: 0
shuffle_song_items: # Définit où les chansons peuvent apparaître.
song: 50
dungeon: 0
any: 0
shopsanity: # Randomise le contenu de la boutique. Réglez sur "off" pour ne pas mélanger les magasins ; "0" mélange les magasins mais ne n'autorise pas les articles multimonde dans les magasins.
0: 0
1: 0
2: 0
3: 0
4: 0
random_value: 0
off: 50
tokensanity : # les récompenses en jetons des Skulltulas dorées sont mélangées dans la réserve.
off: 50
dungeons: 0
overworld: 0
all: 0
shuffle_scrubs: # Mélangez les articles vendus par Business Scrubs et fixez les prix.
off: 50
low: 0
regular: 0
random_prices: 0
shuffle_cows: # les vaches donnent des objets lorsque la chanson d'Epona est jouée.
false: 50
true: 0
shuffle_kokiri_sword: # Mélangez l'épée Kokiri dans la réserve d'objets.
false: 50
true: 0
shuffle_ocarinas: # Mélangez l'Ocarina des fées et l'Ocarina du temps dans la réserve d'objets.
false: 50
true: 0
shuffle_weird_egg: # Mélangez l'œuf bizarre de Malon au château d'Hyrule.
false: 50
true: 0
shuffle_gerudo_card: # Mélangez la carte de membre Gerudo dans la réserve d'objets.
false: 50
true: 0
shuffle_beans: # Ajoute un paquet de 10 haricots au pool d'objets et change le vendeur de haricots pour qu'il vende un objet pour 60 roupies.
false: 50
true: 0
shuffle_medigoron_carpet_salesman: # Mélangez les objets vendus par Medigoron et le vendeur de tapis Haunted Wasteland.
false: 50
true: 0
skip_child_zelda: # le jeu commence avec la lettre de Zelda, l'objet de la berceuse de Zelda et les événements pertinents déjà terminés.
false: 50
true: 0
no_escape_sequence: # Ignore la séquence d'effondrement de la tour entre les combats de Ganondorf et de Ganon.
false: 50
true: 0
no_guard_stealth: # Le vide sanitaire du château d'Hyrule passe directement à Zelda.
false: 50
true: 0
no_epona_race: # Epona peut toujours être invoquée avec Epona's Song.
false: 50
true: 0
skip_some_minigame_phases: # Dampe Race et Horseback Archery donnent les deux récompenses si la deuxième condition est remplie lors de la première tentative.
false: 50
true: 0
complete_mask_quest: # Tous les masques sont immédiatement disponibles à l'emprunt dans la boutique Happy Mask.
false: 50
true: 0
useful_cutscenes: # Réactive la cinématique Poe dans le Temple de la forêt, Darunia dans le Temple du feu et l'introduction de Twinrova. Surtout utile pour les pépins.
false: 50
true: 0
fast_chests: # Toutes les animations des coffres sont rapides. Si désactivé, les éléments principaux ont une animation lente.
false: 50
true: 0
free_scarecrow: # Sortir l'ocarina près d'un point d'épouvantail fait apparaître Pierre sans avoir besoin de la chanson.
false: 50
true: 0
fast_bunny_hood: # Bunny Hood vous permet de vous déplacer 1,5 fois plus vite comme dans Majora's Mask.
false: 50
true: 0
chicken_count: # Contrôle le nombre de Cuccos pour qu'Anju donne un objet en tant qu'enfant.
\# vous pouvez ajouter des valeurs supplémentaires entre le minimum et le maximum
0: 0 # valeur minimale
7: 50 # valeur maximale
random: 0
random-low: 0
random-high: 0
hints: # les pierres à potins peuvent donner des indices sur l'emplacement des objets.
none: 0
mask: 0
agony: 0
always: 50
hint_dist: # Choisissez la distribution d'astuces à utiliser. Affecte la fréquence des indices forts, quels éléments sont toujours indiqués, etc.
balanced: 50
ddr: 0
league: 0
mw2: 0
scrubs: 0
strong: 0
tournament: 0
useless: 0
very_strong: 0
text_shuffle: # Randomise le texte dans le jeu pour un effet comique.
none: 50
except_hints: 0
complete: 0
damage_multiplier: # contrôle la quantité de dégâts subis par Link.
half: 0
normal: 50
double: 0
quadruple: 0
ohko: 0
no_collectible_hearts: # les cœurs ne tomberont pas des ennemis ou des objets.
false: 50
true: 0
starting_tod: # Changer l'heure de début de la journée.
default: 50
sunrise: 0
morning: 0
noon: 0
afternoon: 0
sunset: 0
evening: 0
midnight: 0
witching_hour: 0
start_with_consumables: # Démarrez le jeu avec des Deku Sticks et des Deku Nuts pleins.
false: 50
true: 0
start_with_rupees: # Commencez avec un portefeuille plein. Les mises à niveau de portefeuille rempliront également votre portefeuille.
false: 50
true: 0
item_pool_value: # modifie le nombre d'objets disponibles dans le jeu.
plentiful: 0
balanced: 50
scarce: 0
minimal: 0
junk_ice_traps: # Ajoute des pièges à glace au pool d'objets.
off: 0
normal: 50
on: 0
mayhem: 0
onslaught: 0
ice_trap_appearance: # modifie l'apparence des pièges à glace en tant qu'éléments autonomes.
major_only: 50
junk_only: 0
anything: 0
logic_earliest_adult_trade: # premier élément pouvant apparaître dans la séquence d'échange pour adultes.
pocket_egg: 0
pocket_cucco: 0
cojiro: 0
odd_mushroom: 0
poachers_saw: 0
broken_sword: 0
prescription: 50
eyeball_frog: 0
eyedrops: 0
claim_check: 0
logic_latest_adult_trade: # Dernier élément pouvant apparaître dans la séquence d'échange pour adultes.
pocket_egg: 0
pocket_cucco: 0
cojiro: 0
odd_mushroom: 0
poachers_saw: 0
broken_sword: 0
prescription: 0
eyeball_frog: 0
eyedrops: 0
claim_check: 50
Si vous souhaitez valider votre fichier de configuration pour vous assurer qu'il fonctionne, vous pouvez le faire sur la page YAML Validator. ```
YAML page du validateur : [page de validation YAML](/mysterycheck)
## Rejoindre un jeu multimonde ## Rejoindre une partie MultiWorld
### Obtenez votre fichier OOT modifié ### Obtenez votre fichier de correctif OOT
Lorsque vous rejoignez un jeu multimonde, il vous sera demandé de fournir votre fichier YAML à celui qui l'héberge. Une fois cela fait, l'hébergeur vous fournira soit un lien pour télécharger votre fichier de données, soit un fichier zip contenant les données de chacun des dossiers. Votre fichier de données doit avoir une extension « .apz5 ». Lorsque vous rejoignez un jeu multimonde, il vous sera demandé de fournir votre fichier YAML à l'hébergeur. Une fois que c'est Fini,
l'hébergeur vous fournira soit un lien pour télécharger votre fichier de données, soit un fichier zip contenant les données de chacun
des dossiers. Votre fichier de données doit avoir une extension `.apz5`.
Double-cliquez sur votre fichier « .apz5 » pour démarrer votre client et démarrer le processus de correctif ROM. Une fois le processus terminé (cela peut prendre un certain temps), le client et l'émulateur seront automatiquement démarrés (si vous avez associé l'extension à l'émulateur comme recommandé). Double-cliquez sur votre fichier `.apz5` pour démarrer votre client et démarrer le processus de patch ROM. Une fois le processus terminé
(cela peut prendre un certain temps), le client et l'émulateur seront lancés automatiquement (si vous avez associé l'extension
à l'émulateur comme recommandé).
### Connectez-vous au multiserveur ### Connectez-vous au multiserveur
Une fois le client et l'émulateur démarrés, vous devez les connecter. Accédez à votre dossier d'installation Archipelago, puis vers `data/lua`, et faites glisser et déposez le script `connector_oot.lua` sur la fenêtre principale d'EmuHawk. (Vous pourrez plutôt ouvrir depuis la console Lua manuellement, cliquez sur `Script``Open Script` et accédez à `connector_oot.lua` avec le sélecteur de fichiers.) Une fois le client et l'émulateur démarrés, vous devez les connecter. Dans l'émulateur, cliquez sur "Outils"
menu et sélectionnez "Console Lua". Cliquez sur le bouton du dossier ou appuyez sur Ctrl+O pour ouvrir un script Lua.
Pour connecter le client au multiserveur, mettez simplement `<adresse>:<port>` dans le champ de texte en haut et appuyez sur Entrée (si le serveur utilise un mot de passe, tapez dans le champ de texte inférieur `/connect <adresse>:<port> [mot de passe]`) Accédez à votre dossier d'installation Archipelago et ouvrez `data/lua/connector_oot.lua`.
Vous êtes maintenant prêt à commencer votre aventure dans Hyrule. Pour connecter le client au multiserveur, mettez simplement `<adresse>:<port>` dans le champ de texte en haut et appuyez sur Entrée (si le
le serveur utilise un mot de passe, saisissez dans le champ de texte inférieur `/connect <adresse>:<port> [mot de passe]`)
Vous êtes maintenant prêt à commencer votre aventure à Hyrule.

View File

@@ -1,15 +1,15 @@
# Pokémon Emerald # Pokémon Emerald
## Where is the options page? ## Where is the settings page?
You can read through all the options and generate a YAML [here](../player-options). You can read through all the settings and generate a YAML [here](../player-settings).
## What does randomization do to this game? ## What does randomization do to this game?
This randomizer handles both item randomization and pokémon randomization. Badges, HMs, gifts from NPCs, and items on This randomizer handles both item randomization and pokémon randomization. Badges, HMs, gifts from NPCs, and items on
the ground can all be randomized. There are also many options for randomizing wild pokémon, starters, opponent pokémon, the ground can all be randomized. There are also many options for randomizing wild pokémon, starters, opponent pokémon,
abilities, types, etc… You can even change a percentage of single battles into double battles. Check the abilities, types, etc… You can even change a percentage of single battles into double battles. Check the
[options page](../player-options) for a more comprehensive list of what can be changed. [settings page](../player-settings) for a more comprehensive list of what can be changed.
## What items and locations get randomized? ## What items and locations get randomized?
@@ -28,7 +28,7 @@ randomizer. Here are some of the more important ones:
- You can have both bikes simultaneously - You can have both bikes simultaneously
- You can run or bike (almost) anywhere - You can run or bike (almost) anywhere
- The Wally catching tutorial is skipped - The Wally catching tutorial is skipped
- All text is instant and, with an option, can be automatically progressed by holding A - All text is instant, and with a setting it can be automatically progressed by holding A
- When a Repel runs out, you will be prompted to use another - When a Repel runs out, you will be prompted to use another
- Many more minor improvements… - Many more minor improvements…
@@ -44,7 +44,7 @@ your inventory.
## When the player receives an item, what happens? ## When the player receives an item, what happens?
You will only receive items while in the overworld and not during battles. Depending on your `Receive Item Messages` You will only receive items while in the overworld and not during battles. Depending on your `Receive Item Messages`
option, the received item will either be silently added to your bag or you will be shown a text box with the item's setting, the received item will either be silently added to your bag or you will be shown a text box with the item's
name and the item will be added to your bag while a fanfare plays. name and the item will be added to your bag while a fanfare plays.
## Can I play offline? ## Can I play offline?

View File

@@ -26,8 +26,8 @@ clear it.
## Generating and Patching a Game ## Generating and Patching a Game
1. Create your options file (YAML). You can make one on the 1. Create your settings file (YAML). You can make one on the
[Pokémon Emerald options page](../../../games/Pokemon%20Emerald/player-options). [Pokémon Emerald settings page](../../../games/Pokemon%20Emerald/player-settings).
2. Follow the general Archipelago instructions for [generating a game](../../Archipelago/setup/en#generating-a-game). 2. Follow the general Archipelago instructions for [generating a game](../../Archipelago/setup/en#generating-a-game).
This will generate an output file for you. Your patch file will have the `.apemerald` file extension. This will generate an output file for you. Your patch file will have the `.apemerald` file extension.
3. Open `ArchipelagoLauncher.exe` 3. Open `ArchipelagoLauncher.exe`

View File

@@ -281,20 +281,18 @@ class PokemonRedBlueWorld(World):
self.multiworld.itempool.remove(badge) self.multiworld.itempool.remove(badge)
progitempool.remove(badge) progitempool.remove(badge)
for _ in range(5): for _ in range(5):
badgelocs = [ badgelocs = [self.multiworld.get_location(loc, self.player) for loc in [
self.multiworld.get_location(loc, self.player) for loc in [ "Pewter Gym - Brock Prize", "Cerulean Gym - Misty Prize",
"Pewter Gym - Brock Prize", "Cerulean Gym - Misty Prize", "Vermilion Gym - Lt. Surge Prize", "Celadon Gym - Erika Prize",
"Vermilion Gym - Lt. Surge Prize", "Celadon Gym - Erika Prize", "Fuchsia Gym - Koga Prize", "Saffron Gym - Sabrina Prize",
"Fuchsia Gym - Koga Prize", "Saffron Gym - Sabrina Prize", "Cinnabar Gym - Blaine Prize", "Viridian Gym - Giovanni Prize"]]
"Cinnabar Gym - Blaine Prize", "Viridian Gym - Giovanni Prize"
] if self.multiworld.get_location(loc, self.player).item is None]
state = self.multiworld.get_all_state(False) state = self.multiworld.get_all_state(False)
self.multiworld.random.shuffle(badges) self.multiworld.random.shuffle(badges)
self.multiworld.random.shuffle(badgelocs) self.multiworld.random.shuffle(badgelocs)
badgelocs_copy = badgelocs.copy() badgelocs_copy = badgelocs.copy()
# allow_partial so that unplaced badges aren't lost, for debugging purposes # allow_partial so that unplaced badges aren't lost, for debugging purposes
fill_restrictive(self.multiworld, state, badgelocs_copy, badges, True, True, allow_partial=True) fill_restrictive(self.multiworld, state, badgelocs_copy, badges, True, True, allow_partial=True)
if len(badges) > 8 - len(badgelocs): if badges:
for location in badgelocs: for location in badgelocs:
if location.item: if location.item:
badges.append(location.item) badges.append(location.item)
@@ -304,7 +302,6 @@ class PokemonRedBlueWorld(World):
for location in badgelocs: for location in badgelocs:
if location.item: if location.item:
fill_locations.remove(location) fill_locations.remove(location)
progitempool += badges
break break
else: else:
raise FillError(f"Failed to place badges for player {self.player}") raise FillError(f"Failed to place badges for player {self.player}")

View File

@@ -157,7 +157,7 @@ def get_rules_lookup(player: int):
"Puzzle Solved Underground Elevator": lambda state: ((state.can_reach("Underground Lake", "Region", player) or state.can_reach("Office", "Region", player) "Puzzle Solved Underground Elevator": lambda state: ((state.can_reach("Underground Lake", "Region", player) or state.can_reach("Office", "Region", player)
and state.has("Key for Office Elevator", player))), and state.has("Key for Office Elevator", player))),
"Puzzle Solved Bedroom Elevator": lambda state: (state.can_reach("Office", "Region", player) and state.has_all({"Key for Bedroom Elevator","Crawling"}, player)), "Puzzle Solved Bedroom Elevator": lambda state: (state.can_reach("Office", "Region", player) and state.has_all({"Key for Bedroom Elevator","Crawling"}, player)),
"Puzzle Solved Three Floor Elevator": lambda state: (((state.can_reach("Maintenance Tunnels", "Region", player) or state.can_reach("Blue Maze", "Region", player)) "Puzzle Solved Three Floor Elevator": lambda state: ((state.can_reach("Maintenance Tunnels", "Region", player) or state.can_reach("Blue Maze", "Region", player)
and state.has("Key for Three Floor Elevator", player))) and state.has("Key for Three Floor Elevator", player)))
}, },
"lightning": { "lightning": {

View File

@@ -61,7 +61,7 @@ class SMSNIClient(SNIClient):
from SNIClient import snes_buffered_write, snes_flush_writes, snes_read from SNIClient import snes_buffered_write, snes_flush_writes, snes_read
rom_name = await snes_read(ctx, SM_ROMNAME_START, ROMNAME_SIZE) 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[2] not in b"1234567890": 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 return False
ctx.game = self.game ctx.game = self.game

View File

@@ -21,7 +21,7 @@ def fix_reg(entrance_map: dict, entrance: SM64Levels, invalid_regions: set,
def set_rules(world, player: int, area_connections: dict): def set_rules(world, player: int, area_connections: dict):
randomized_level_to_paintings = sm64_level_to_paintings.copy() randomized_level_to_paintings = sm64_level_to_paintings.copy()
randomized_level_to_secrets = sm64_level_to_secrets.copy() randomized_level_to_secrets = sm64_level_to_secrets.copy()
if world.AreaRandomizer[player].value >= 1: # Some randomization is happening, randomize Courses if world.AreaRandomizer[player].value == 1: # Some randomization is happening, randomize Courses
randomized_level_to_paintings = shuffle_dict_keys(world,sm64_level_to_paintings) randomized_level_to_paintings = shuffle_dict_keys(world,sm64_level_to_paintings)
if world.AreaRandomizer[player].value == 2: # Randomize Secrets as well if world.AreaRandomizer[player].value == 2: # Randomize Secrets as well
randomized_level_to_secrets = shuffle_dict_keys(world,sm64_level_to_secrets) randomized_level_to_secrets = shuffle_dict_keys(world,sm64_level_to_secrets)

View File

@@ -69,7 +69,7 @@ class SMZ3SNIClient(SNIClient):
ctx.finished_game = True ctx.finished_game = True
return return
data = await snes_read(ctx, SMZ3_RECV_PROGRESS_ADDR + 0xD3C, 4) data = await snes_read(ctx, SMZ3_RECV_PROGRESS_ADDR + 0x680, 4)
if data is None: if data is None:
return return
@@ -77,14 +77,14 @@ class SMZ3SNIClient(SNIClient):
recv_item = data[2] | (data[3] << 8) recv_item = data[2] | (data[3] << 8)
while (recv_index < recv_item): while (recv_index < recv_item):
item_address = recv_index * 2 item_address = recv_index * 8
message = await snes_read(ctx, SMZ3_RECV_PROGRESS_ADDR + 0xDA0 + item_address, 2) message = await snes_read(ctx, SMZ3_RECV_PROGRESS_ADDR + 0x700 + item_address, 8)
is_z3_item = ((message[1] & 0x80) != 0) is_z3_item = ((message[5] & 0x80) != 0)
masked_part = (message[1] & 0x7F) if is_z3_item else message[1] masked_part = (message[5] & 0x7F) if is_z3_item else message[5]
item_index = ((message[0] | (masked_part << 8)) >> 3) + (256 if is_z3_item else 0) item_index = ((message[4] | (masked_part << 8)) >> 3) + (256 if is_z3_item else 0)
recv_index += 1 recv_index += 1
snes_buffered_write(ctx, SMZ3_RECV_PROGRESS_ADDR + 0xD3C, bytes([recv_index & 0xFF, (recv_index >> 8) & 0xFF])) snes_buffered_write(ctx, SMZ3_RECV_PROGRESS_ADDR + 0x680, bytes([recv_index & 0xFF, (recv_index >> 8) & 0xFF]))
from .TotalSMZ3.Location import locations_start_id from .TotalSMZ3.Location import locations_start_id
from . import convertLocSMZ3IDToAPID from . import convertLocSMZ3IDToAPID
@@ -95,7 +95,7 @@ class SMZ3SNIClient(SNIClient):
snes_logger.info(f'New Check: {location} ({len(ctx.locations_checked)}/{len(ctx.missing_locations) + len(ctx.checked_locations)})') 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]}]) await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": [location_id]}])
data = await snes_read(ctx, SMZ3_RECV_PROGRESS_ADDR + 0xD36, 4) data = await snes_read(ctx, SMZ3_RECV_PROGRESS_ADDR + 0x600, 4)
if data is None: if data is None:
return return
@@ -106,10 +106,10 @@ class SMZ3SNIClient(SNIClient):
item = ctx.items_received[item_out_ptr] item = ctx.items_received[item_out_ptr]
item_id = item.item - items_start_id item_id = item.item - items_start_id
player_id = item.player if item.player < SMZ3_ROM_PLAYER_LIMIT else 0 player_id = item.player if item.player <= SMZ3_ROM_PLAYER_LIMIT else 0
snes_buffered_write(ctx, SMZ3_RECV_PROGRESS_ADDR + item_out_ptr * 2, bytes([player_id, item_id])) 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 item_out_ptr += 1
snes_buffered_write(ctx, SMZ3_RECV_PROGRESS_ADDR + 0xD38, bytes([item_out_ptr & 0xFF, (item_out_ptr >> 8) & 0xFF])) 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)' % ( 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'), 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))) ctx.location_names[item.location], item_out_ptr, len(ctx.items_received)))

View File

@@ -80,8 +80,7 @@ class SMZ3World(World):
locationNamesGT: Set[str] = {loc.Name for loc in GanonsTower(None, None).Locations} locationNamesGT: Set[str] = {loc.Name for loc in GanonsTower(None, None).Locations}
# first added for 0.2.6 # first added for 0.2.6
# optimized message queues for 0.4.4 required_client_version = (0, 2, 6)
required_client_version = (0, 4, 4)
def __init__(self, world: MultiWorld, player: int): def __init__(self, world: MultiWorld, player: int):
self.rom_name_available_event = threading.Event() self.rom_name_available_event = threading.Event()

Binary file not shown.

View File

@@ -93,11 +93,7 @@ def get_pool_core(world):
# Starting Weapon # Starting Weapon
start_weapon_locations = starting_weapon_locations.copy() start_weapon_locations = starting_weapon_locations.copy()
final_starting_weapons = [weapon for weapon in starting_weapons starting_weapon = random.choice(starting_weapons)
if weapon not in world.multiworld.non_local_items[world.player]]
if not final_starting_weapons:
final_starting_weapons = starting_weapons
starting_weapon = random.choice(final_starting_weapons)
if world.multiworld.StartingPosition[world.player] == StartingPosition.option_safe: if world.multiworld.StartingPosition[world.player] == StartingPosition.option_safe:
placed_items[start_weapon_locations[0]] = starting_weapon placed_items[start_weapon_locations[0]] = starting_weapon
elif world.multiworld.StartingPosition[world.player] in \ elif world.multiworld.StartingPosition[world.player] in \

View File

@@ -200,17 +200,15 @@ class TLoZWorld(World):
for i in range(0, 0x7F): for i in range(0, 0x7F):
item = rom_data[first_quest_dungeon_items_early + i] item = rom_data[first_quest_dungeon_items_early + i]
if item & 0b00100000: if item & 0b00100000:
item = item & 0b11011111 rom_data[first_quest_dungeon_items_early + i] = item & 0b11011111
item = item | 0b01000000 rom_data[first_quest_dungeon_items_early + i] = item | 0b01000000
rom_data[first_quest_dungeon_items_early + i] = item
if item & 0b00011111 == 0b00000011: # Change all Item 03s to Item 3F, the proper "nothing" if item & 0b00011111 == 0b00000011: # Change all Item 03s to Item 3F, the proper "nothing"
rom_data[first_quest_dungeon_items_early + i] = item | 0b00111111 rom_data[first_quest_dungeon_items_early + i] = item | 0b00111111
item = rom_data[first_quest_dungeon_items_late + i] item = rom_data[first_quest_dungeon_items_late + i]
if item & 0b00100000: if item & 0b00100000:
item = item & 0b11011111 rom_data[first_quest_dungeon_items_late + i] = item & 0b11011111
item = item | 0b01000000 rom_data[first_quest_dungeon_items_late + i] = item | 0b01000000
rom_data[first_quest_dungeon_items_late + i] = item
if item & 0b00011111 == 0b00000011: if item & 0b00011111 == 0b00000011:
rom_data[first_quest_dungeon_items_late + i] = item | 0b00111111 rom_data[first_quest_dungeon_items_late + i] = item | 0b00111111
return rom_data return rom_data

View File

@@ -143,7 +143,7 @@ class WitnessWorld(World):
# Pick an early item to place on the tutorial gate. # Pick an early item to place on the tutorial gate.
early_items = [item for item in self.items.get_early_items() if item in self.items.get_mandatory_items()] early_items = [item for item in self.items.get_early_items() if item in self.items.get_mandatory_items()]
if early_items: if early_items:
random_early_item = self.random.choice(early_items) random_early_item = self.multiworld.random.choice(early_items)
if self.options.puzzle_randomization == 1: if self.options.puzzle_randomization == 1:
# In Expert, only tag the item as early, rather than forcing it onto the gate. # In Expert, only tag the item as early, rather than forcing it onto the gate.
self.multiworld.local_early_items[self.player][random_early_item] = 1 self.multiworld.local_early_items[self.player][random_early_item] = 1

View File

@@ -1,501 +0,0 @@
import asyncio
import base64
import platform
from typing import Any, ClassVar, Coroutine, Dict, List, Optional, Protocol, Tuple, cast
from CommonClient import CommonContext, server_loop, gui_enabled, \
ClientCommandProcessor, logger, get_base_parser
from NetUtils import ClientStatus
from Utils import async_start
import colorama
from zilliandomizer.zri.memory import Memory
from zilliandomizer.zri import events
from zilliandomizer.utils.loc_name_maps import id_to_loc
from zilliandomizer.options import Chars
from zilliandomizer.patch import RescueInfo
from .id_maps import make_id_to_others
from .config import base_id, zillion_map
class ZillionCommandProcessor(ClientCommandProcessor):
ctx: "ZillionContext"
def _cmd_sms(self) -> None:
""" Tell the client that Zillion is running in RetroArch. """
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"
command_processor = ZillionCommandProcessor
items_handling = 1 # receive items from other players
known_name: Optional[str]
""" This is almost the same as `auth` except `auth` is reset to `None` when server disconnects, and this isn't. """
from_game: "asyncio.Queue[events.EventFromGame]"
to_game: "asyncio.Queue[events.EventToGame]"
ap_local_count: int
""" local checks watched by server """
next_item: int
""" index in `items_received` """
ap_id_to_name: Dict[int, str]
ap_id_to_zz_id: Dict[int, int]
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 """
look_for_retroarch: asyncio.Event
"""
There is a bug in Python in Windows
https://github.com/python/cpython/issues/91227
that makes it so if I look for RetroArch before it's ready,
it breaks the asyncio udp transport system.
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.known_name = None
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":
# asyncio udp bug is only on Windows
self.look_for_retroarch.set()
self.reset_game_state()
def reset_game_state(self) -> None:
for _ in range(self.from_game.qsize()):
self.from_game.get_nowait()
for _ in range(self.to_game.qsize()):
self.to_game.get_nowait()
self.got_slot_data.clear()
self.ap_local_count = 0
self.next_item = 0
self.ap_id_to_name = {}
self.ap_id_to_zz_id = {}
self.rescues = {}
self.loc_mem_to_id = {}
self.locations_checked.clear()
self.missing_locations.clear()
self.checked_locations.clear()
self.finished_game = False
self.items_received.clear()
# override
def on_deathlink(self, data: Dict[str, Any]) -> None:
self.to_game.put_nowait(events.DeathEventToGame())
return super().on_deathlink(data)
# override
async def server_auth(self, password_requested: bool = False) -> None:
if password_requested and not self.password:
await super().server_auth(password_requested)
if not self.auth:
logger.info('waiting for connection to game...')
return
logger.info("logging in to server...")
await self.send_connect()
# 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 = [
("Client", "Archipelago")
]
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)
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:
logger.warn("`Connected` packet missing `slot_data`")
return
slot_data = args["slot_data"]
if "start_char" not in slot_data:
logger.warn("invalid Zillion `Connected` packet, `slot_data` missing `start_char`")
return
self.start_char = slot_data['start_char']
if self.start_char not in {"Apple", "Champ", "JJ"}:
logger.warn("invalid Zillion `Connected` packet, "
f"`slot_data` `start_char` has invalid value: {self.start_char}")
if "rescues" not in slot_data:
logger.warn("invalid Zillion `Connected` packet, `slot_data` missing `rescues`")
return
rescues = slot_data["rescues"]
self.rescues = {}
for rescue_id, json_info in rescues.items():
assert rescue_id in ("0", "1"), f"invalid rescue_id in Zillion slot_data: {rescue_id}"
# TODO: just take start_char out of the RescueInfo so there's no opportunity for a mismatch?
assert json_info["start_char"] == self.start_char, \
f'mismatch in Zillion slot data: {json_info["start_char"]} {self.start_char}'
ri = RescueInfo(json_info["start_char"],
json_info["room_code"],
json_info["mask"])
self.rescues[0 if rescue_id == "0" else 1] = ri
if "loc_mem_to_id" not in slot_data:
logger.warn("invalid Zillion `Connected` packet, `slot_data` missing `loc_mem_to_id`")
return
loc_mem_to_id = slot_data["loc_mem_to_id"]
self.loc_mem_to_id = {}
for mem_str, id_str in loc_mem_to_id.items():
mem = int(mem_str)
id_ = int(id_str)
room_i = mem // 256
assert 0 <= room_i < 74
assert id_ in id_to_loc
self.loc_mem_to_id[mem] = id_
if len(self.loc_mem_to_id) != 394:
logger.warn("invalid Zillion `Connected` packet, "
f"`slot_data` missing locations in `loc_mem_to_id` - len {len(self.loc_mem_to_id)}")
self.got_slot_data.set()
payload = {
"cmd": "Get",
"keys": [f"zillion-{self.auth}-doors"]
}
async_start(self.send_msgs([payload]))
elif cmd == "Retrieved":
if "keys" not in args:
logger.warning(f"invalid Retrieved packet to ZillionClient: {args}")
return
keys = cast(Dict[str, Optional[str]], args["keys"])
doors_b64 = keys.get(f"zillion-{self.auth}-doors", None)
if doors_b64:
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():
event_from_game = self.from_game.get_nowait()
if isinstance(event_from_game, events.AcquireLocationEventFromGame):
server_id = event_from_game.id + base_id
loc_name = id_to_loc[event_from_game.id]
self.locations_checked.add(server_id)
if server_id in self.missing_locations:
self.ap_local_count += 1
n_locations = len(self.missing_locations) + len(self.checked_locations) - 1 # -1 to ignore win
logger.info(f'New Check: {loc_name} ({self.ap_local_count}/{n_locations})')
async_start(self.send_msgs([
{"cmd": 'LocationChecks', "locations": [server_id]}
]))
else:
# This will happen a lot in Zillion,
# because all the key words are local and unwatched by the server.
logger.debug(f"DEBUG: {loc_name} not in missing")
elif isinstance(event_from_game, events.DeathEventFromGame):
async_start(self.send_death())
elif isinstance(event_from_game, events.WinEventFromGame):
if not self.finished_game:
async_start(self.send_msgs([
{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}
]))
self.finished_game = True
elif isinstance(event_from_game, events.DoorEventFromGame):
if self.auth:
doors_b64 = base64.b64encode(event_from_game.doors).decode()
payload = {
"cmd": "Set",
"key": f"zillion-{self.auth}-doors",
"operations": [{"operation": "replace", "value": doors_b64}]
}
async_start(self.send_msgs([payload]))
else:
logger.warning(f"WARNING: unhandled event from game {event_from_game}")
def process_items_received(self) -> None:
if len(self.items_received) > self.next_item:
zz_item_ids = [self.ap_id_to_zz_id[item.item] for item in self.items_received]
for index in range(self.next_item, len(self.items_received)):
ap_id = self.items_received[index].item
from_name = self.player_names[self.items_received[index].player]
# TODO: colors in this text, like sni client?
logger.info(f'received {self.ap_id_to_name[ap_id]} from {from_name}')
self.to_game.put_nowait(
events.ItemEventToGame(zz_item_ids)
)
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")
# to work around the Python bug where we can't check for RetroArch
if not ctx.look_for_retroarch.is_set():
logger.info("Start Zillion in RetroArch, then use the /sms command to connect to it.")
await asyncio.wait((
asyncio.create_task(ctx.look_for_retroarch.wait()),
asyncio.create_task(ctx.exit_event.wait())
), return_when=asyncio.FIRST_COMPLETED)
last_log = ""
def log_no_spam(msg: str) -> None:
nonlocal last_log
if msg != last_log:
last_log = msg
logger.info(msg)
# to only show this message once per client run
help_message_shown = False
with Memory(ctx.from_game, ctx.to_game) as memory:
while not ctx.exit_event.is_set():
ram = await memory.read()
game_id = memory.get_rom_to_ram_data(ram)
name, seed_end = name_seed_from_ram(game_id)
if len(name):
if name == ctx.known_name:
ctx.auth = name
# this is the name we know
if ctx.server and ctx.server.socket: # type: ignore
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
async_start(ctx.send_connect())
log_no_spam("logging in to server...")
await asyncio.wait((
asyncio.create_task(ctx.got_slot_data.wait()),
asyncio.create_task(ctx.exit_event.wait()),
asyncio.create_task(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
ctx.known_name = name
async_start(ctx.connect())
await asyncio.wait((
asyncio.create_task(ctx.got_room_info.wait()),
asyncio.create_task(ctx.exit_event.wait()),
asyncio.create_task(asyncio.sleep(6))
), 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.')
help_message_shown = True
log_no_spam("looking for connection to game...")
await asyncio.sleep(0.3)
await asyncio.sleep(0.09375)
logger.info("zillion sync task ending")
async def main() -> None:
parser = get_base_parser()
parser.add_argument('diff_file', default="", type=str, nargs="?",
help='Path to a .apzl Archipelago Binary Patch file')
# SNI parser.add_argument('--loglevel', default='info', choices=['debug', 'info', 'warning', 'error', 'critical'])
args = parser.parse_args()
print(args)
if args.diff_file:
import Patch
logger.info("patch file was supplied - creating sms rom...")
meta, rom_file = Patch.create_rom_file(args.diff_file)
if "server" in meta:
args.connect = meta["server"]
logger.info(f"wrote rom file to {rom_file}")
ctx = ZillionContext(args.connect, args.password)
if ctx.server_task is None:
ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop")
if gui_enabled:
ctx.run_gui()
ctx.run_cli()
sync_task = asyncio.create_task(zillion_sync_task(ctx))
await ctx.exit_event.wait()
ctx.server_address = None
logger.debug("waiting for sync task to end")
await sync_task
logger.debug("sync task ended")
await ctx.shutdown()
def launch() -> None:
colorama.init()
asyncio.run(main())
colorama.deinit()