Compare commits

..

1 Commits

Author SHA1 Message Date
CaitSith2 3738399348 Factorio: Add an Allow Collect Option 2023-11-26 21:25:43 -08:00
139 changed files with 1649 additions and 2030 deletions
+5 -6
View File
@@ -115,12 +115,11 @@ class AdventureContext(CommonContext):
msg = f"Received {', '.join([self.item_names[item.item] for item in args['items']])}"
self._set_message(msg, SYSTEM_MESSAGE_ID)
elif cmd == "Retrieved":
if f"adventure_{self.auth}_freeincarnates_used" in args["keys"]:
self.freeincarnates_used = args["keys"][f"adventure_{self.auth}_freeincarnates_used"]
if self.freeincarnates_used is None:
self.freeincarnates_used = 0
self.freeincarnates_used += self.freeincarnate_pending
self.send_pending_freeincarnates()
self.freeincarnates_used = args["keys"][f"adventure_{self.auth}_freeincarnates_used"]
if self.freeincarnates_used is None:
self.freeincarnates_used = 0
self.freeincarnates_used += self.freeincarnate_pending
self.send_pending_freeincarnates()
elif cmd == "SetReply":
if args["key"] == f"adventure_{self.auth}_freeincarnates_used":
self.freeincarnates_used = args["value"]
+21 -33
View File
@@ -252,20 +252,15 @@ class MultiWorld():
range(1, self.players + 1)}
def set_options(self, args: Namespace) -> None:
# TODO - remove this section once all worlds use options dataclasses
all_keys: Set[str] = {key for player in self.player_ids for key in
AutoWorld.AutoWorldRegister.world_types[self.game[player]].options_dataclass.type_hints}
for option_key in all_keys:
option = Utils.DeprecateDict(f"Getting options from multiworld is now deprecated. "
f"Please use `self.options.{option_key}` instead.")
option.update(getattr(args, option_key, {}))
setattr(self, option_key, option)
for player in self.player_ids:
world_type = AutoWorld.AutoWorldRegister.world_types[self.game[player]]
self.worlds[player] = world_type(self, player)
self.worlds[player].random = self.per_slot_randoms[player]
options_dataclass: typing.Type[Options.PerGameCommonOptions] = world_type.options_dataclass
for option_key in world_type.options_dataclass.type_hints:
option_values = getattr(args, option_key, {})
setattr(self, option_key, option_values)
# TODO - remove this loop once all worlds use options dataclasses
options_dataclass: typing.Type[Options.PerGameCommonOptions] = self.worlds[player].options_dataclass
self.worlds[player].options = options_dataclass(**{option_key: getattr(args, option_key)[player]
for option_key in options_dataclass.type_hints})
@@ -496,7 +491,7 @@ class MultiWorld():
else:
return all((self.has_beaten_game(state, p) for p in range(1, self.players + 1)))
def can_beat_game(self, starting_state: Optional[CollectionState] = None) -> bool:
def can_beat_game(self, starting_state: Optional[CollectionState] = None):
if starting_state:
if self.has_beaten_game(starting_state):
return True
@@ -509,7 +504,7 @@ class MultiWorld():
and location.item.advancement and location not in state.locations_checked}
while prog_locations:
sphere: Set[Location] = set()
sphere = set()
# build up spheres of collection radius.
# Everything in each sphere is independent from each other in dependencies and only depends on lower spheres
for location in prog_locations:
@@ -529,19 +524,12 @@ class MultiWorld():
return False
def get_spheres(self) -> Iterator[Set[Location]]:
"""
yields a set of locations for each logical sphere
If there are unreachable locations, the last sphere of reachable
locations is followed by an empty set, and then a set of all of the
unreachable locations.
"""
def get_spheres(self):
state = CollectionState(self)
locations = set(self.get_filled_locations())
while locations:
sphere: Set[Location] = set()
sphere = set()
for location in locations:
if location.can_reach(state):
@@ -651,34 +639,34 @@ class CollectionState():
def update_reachable_regions(self, player: int):
self.stale[player] = False
reachable_regions = self.reachable_regions[player]
blocked_connections = self.blocked_connections[player]
rrp = self.reachable_regions[player]
bc = self.blocked_connections[player]
queue = deque(self.blocked_connections[player])
start = self.multiworld.get_region("Menu", player)
start = self.multiworld.get_region('Menu', player)
# init on first call - this can't be done on construction since the regions don't exist yet
if start not in reachable_regions:
reachable_regions.add(start)
blocked_connections.update(start.exits)
if start not in rrp:
rrp.add(start)
bc.update(start.exits)
queue.extend(start.exits)
# run BFS on all connections, and keep track of those blocked by missing items
while queue:
connection = queue.popleft()
new_region = connection.connected_region
if new_region in reachable_regions:
blocked_connections.remove(connection)
if new_region in rrp:
bc.remove(connection)
elif connection.can_reach(self):
assert new_region, f"tried to search through an Entrance \"{connection}\" with no Region"
reachable_regions.add(new_region)
blocked_connections.remove(connection)
blocked_connections.update(new_region.exits)
rrp.add(new_region)
bc.remove(connection)
bc.update(new_region.exits)
queue.extend(new_region.exits)
self.path[new_region] = (new_region.name, self.path.get(connection, None))
# Retry connections if the new region can unblock them
for new_entrance in self.multiworld.indirect_connections.get(new_region, set()):
if new_entrance in blocked_connections and new_entrance not in queue:
if new_entrance in bc and new_entrance not in queue:
queue.append(new_entrance)
def copy(self) -> CollectionState:
+21 -20
View File
@@ -550,7 +550,7 @@ def flood_items(world: MultiWorld) -> None:
break
def balance_multiworld_progression(multiworld: MultiWorld) -> None:
def balance_multiworld_progression(world: MultiWorld) -> None:
# A system to reduce situations where players have no checks remaining, popularly known as "BK mode."
# Overall progression balancing algorithm:
# Gather up all locations in a sphere.
@@ -558,28 +558,28 @@ def balance_multiworld_progression(multiworld: MultiWorld) -> None:
# If other players are below the threshold value, swap progression in this sphere into earlier spheres,
# which gives more locations available by this sphere.
balanceable_players: typing.Dict[int, float] = {
player: multiworld.worlds[player].options.progression_balancing / 100
for player in multiworld.player_ids
if multiworld.worlds[player].options.progression_balancing > 0
player: world.worlds[player].options.progression_balancing / 100
for player in world.player_ids
if world.worlds[player].options.progression_balancing > 0
}
if not balanceable_players:
logging.info('Skipping multiworld progression balancing.')
else:
logging.info(f'Balancing multiworld progression for {len(balanceable_players)} Players.')
logging.debug(balanceable_players)
state: CollectionState = CollectionState(multiworld)
state: CollectionState = CollectionState(world)
checked_locations: typing.Set[Location] = set()
unchecked_locations: typing.Set[Location] = set(multiworld.get_locations())
unchecked_locations: typing.Set[Location] = set(world.get_locations())
total_locations_count: typing.Counter[int] = Counter(
location.player
for location in multiworld.get_locations()
for location in world.get_locations()
if not location.locked
)
reachable_locations_count: typing.Dict[int, int] = {
player: 0
for player in multiworld.player_ids
if total_locations_count[player] and len(multiworld.get_filled_locations(player)) != 0
for player in world.player_ids
if total_locations_count[player] and len(world.get_filled_locations(player)) != 0
}
balanceable_players = {
player: balanceable_players[player]
@@ -658,7 +658,7 @@ def balance_multiworld_progression(multiworld: MultiWorld) -> None:
balancing_unchecked_locations.remove(location)
if not location.locked:
balancing_reachables[location.player] += 1
if multiworld.has_beaten_game(balancing_state) or all(
if world.has_beaten_game(balancing_state) or all(
item_percentage(player, reachables) >= threshold_percentages[player]
for player, reachables in balancing_reachables.items()
if player in threshold_percentages):
@@ -675,7 +675,7 @@ def balance_multiworld_progression(multiworld: MultiWorld) -> None:
locations_to_test = unlocked_locations[player]
items_to_test = list(candidate_items[player])
items_to_test.sort()
multiworld.random.shuffle(items_to_test)
world.random.shuffle(items_to_test)
while items_to_test:
testing = items_to_test.pop()
reducing_state = state.copy()
@@ -687,8 +687,8 @@ def balance_multiworld_progression(multiworld: MultiWorld) -> None:
reducing_state.sweep_for_events(locations=locations_to_test)
if multiworld.has_beaten_game(balancing_state):
if not multiworld.has_beaten_game(reducing_state):
if world.has_beaten_game(balancing_state):
if not world.has_beaten_game(reducing_state):
items_to_replace.append(testing)
else:
reduced_sphere = get_sphere_locations(reducing_state, locations_to_test)
@@ -696,32 +696,33 @@ def balance_multiworld_progression(multiworld: MultiWorld) -> None:
if p < threshold_percentages[player]:
items_to_replace.append(testing)
old_moved_item_count = moved_item_count
replaced_items = False
# sort then shuffle to maintain deterministic behaviour,
# while allowing use of set for better algorithm growth behaviour elsewhere
replacement_locations = sorted(l for l in checked_locations if not l.event and not l.locked)
multiworld.random.shuffle(replacement_locations)
world.random.shuffle(replacement_locations)
items_to_replace.sort()
multiworld.random.shuffle(items_to_replace)
world.random.shuffle(items_to_replace)
# Start swapping items. Since we swap into earlier spheres, no need for accessibility checks.
while replacement_locations and items_to_replace:
old_location = items_to_replace.pop()
for i, new_location in enumerate(replacement_locations):
for new_location in replacement_locations:
if new_location.can_fill(state, old_location.item, False) and \
old_location.can_fill(state, new_location.item, False):
replacement_locations.pop(i)
replacement_locations.remove(new_location)
swap_location_item(old_location, new_location)
logging.debug(f"Progression balancing moved {new_location.item} to {new_location}, "
f"displacing {old_location.item} into {old_location}")
moved_item_count += 1
state.collect(new_location.item, True, new_location)
replaced_items = True
break
else:
logging.warning(f"Could not Progression Balance {old_location.item}")
if old_moved_item_count < moved_item_count:
if replaced_items:
logging.debug(f"Moved {moved_item_count} items so far\n")
unlocked = {fresh for player in balancing_players for fresh in unlocked_locations[player]}
for location in get_sphere_locations(state, unlocked):
@@ -735,7 +736,7 @@ def balance_multiworld_progression(multiworld: MultiWorld) -> None:
state.collect(location.item, True, location)
checked_locations |= sphere_locations
if multiworld.has_beaten_game(state):
if world.has_beaten_game(state):
break
elif not sphere_locations:
logging.warning("Progression Balancing ran out of paths.")
-11
View File
@@ -117,17 +117,6 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
for item_name, count in world.start_inventory_from_pool.setdefault(player, StartInventoryPool({})).value.items():
for _ in range(count):
world.push_precollected(world.create_item(item_name, player))
# remove from_pool items also from early items handling, as starting is plenty early.
early = world.early_items[player].get(item_name, 0)
if early:
world.early_items[player][item_name] = max(0, early-count)
remaining_count = count-early
if remaining_count > 0:
local_early = world.early_local_items[player].get(item_name, 0)
if local_early:
world.early_items[player][item_name] = max(0, local_early - remaining_count)
del local_early
del early
logger.info('Creating World.')
AutoWorld.call_all(world, "create_regions")
+5
View File
@@ -1033,6 +1033,11 @@ class DeathLink(Toggle):
display_name = "Death Link"
class AllowCollect(DefaultOnToggle):
"""Allows checks in your world to be automatically marked as collected when !collect is run."""
display_name = "Allow Collect"
class ItemLinks(OptionList):
"""Share part of your item pool with other players."""
display_name = "Item Links"
-19
View File
@@ -779,25 +779,6 @@ def deprecate(message: str):
import warnings
warnings.warn(message)
class DeprecateDict(dict):
log_message: str
should_error: bool
def __init__(self, message, error: bool = False) -> None:
self.log_message = message
self.should_error = error
super().__init__()
if __debug__:
def __getitem__(self, item: Any) -> Any:
if self.should_error:
deprecate(self.log_message)
import warnings
warnings.warn(self.log_message)
return super().__getitem__(item)
def _extend_freeze_support() -> None:
"""Extend multiprocessing.freeze_support() to also work on Non-Windows for spawn."""
# upstream issue: https://github.com/python/cpython/issues/76327
+26 -29
View File
@@ -1,4 +1,3 @@
import os
import zipfile
import base64
from typing import Union, Dict, Set, Tuple
@@ -7,7 +6,13 @@ from flask import request, flash, redirect, url_for, render_template
from markupsafe import Markup
from WebHostLib import app
from WebHostLib.upload import allowed_options, allowed_options_extensions, banned_file
banned_zip_contents = (".sfc",)
def allowed_file(filename):
return filename.endswith(('.txt', ".yaml", ".zip"))
from Generate import roll_settings, PlandoOptions
from Utils import parse_yamls
@@ -46,41 +51,33 @@ def mysterycheck():
def get_yaml_data(files) -> Union[Dict[str, str], str, Markup]:
options = {}
for uploaded_file in files:
if banned_file(uploaded_file.filename):
return ("Uploaded data contained a rom file, which is likely to contain copyrighted material. "
"Your file was deleted.")
# If the user does not select file, the browser will still submit an empty string without a file name.
elif uploaded_file.filename == "":
return "No selected file."
# if user does not select file, browser also
# submit an empty part without filename
if uploaded_file.filename == '':
return 'No selected file'
elif uploaded_file.filename in options:
return f"Conflicting files named {uploaded_file.filename} submitted."
elif uploaded_file and allowed_options(uploaded_file.filename):
return f'Conflicting files named {uploaded_file.filename} submitted'
elif uploaded_file and allowed_file(uploaded_file.filename):
if uploaded_file.filename.endswith(".zip"):
if not zipfile.is_zipfile(uploaded_file):
return f"Uploaded file {uploaded_file.filename} is not a valid .zip file and cannot be opened."
uploaded_file.seek(0) # offset from is_zipfile check
with zipfile.ZipFile(uploaded_file, "r") as zfile:
for file in zfile.infolist():
# Remove folder pathing from str (e.g. "__MACOSX/" folder paths from archives created by macOS).
base_filename = os.path.basename(file.filename)
with zipfile.ZipFile(uploaded_file, 'r') as zfile:
infolist = zfile.infolist()
if base_filename.endswith(".archipelago"):
return Markup("Error: Your .zip file contains an .archipelago file. "
'Did you mean to <a href="/uploads">host a game</a>?')
elif base_filename.endswith(".zip"):
return "Nested .zip files inside a .zip are not supported."
elif banned_file(base_filename):
return ("Uploaded data contained a rom file, which is likely to contain copyrighted "
"material. Your file was deleted.")
# Ignore dot-files.
elif not base_filename.startswith(".") and allowed_options(base_filename):
if any(file.filename.endswith(".archipelago") for file in infolist):
return Markup("Error: Your .zip file contains an .archipelago file. "
'Did you mean to <a href="/uploads">host a game</a>?')
for file in infolist:
if file.filename.endswith(banned_zip_contents):
return ("Uploaded data contained a rom file, "
"which is likely to contain copyrighted material. "
"Your file was deleted.")
elif file.filename.endswith((".yaml", ".json", ".yml", ".txt")):
options[file.filename] = zfile.open(file, "r").read()
else:
options[uploaded_file.filename] = uploaded_file.read()
if not options:
return f"Did not find any valid files to process. Accepted formats: {allowed_options_extensions}"
return "Did not find a .yaml file to process."
return options
-6
View File
@@ -205,12 +205,6 @@ def run_server_process(room_id, ponyconfig: dict, static_server_data: dict,
ctx.auto_shutdown = Room.get(id=room_id).timeout
ctx.shutdown_task = asyncio.create_task(auto_shutdown(ctx, []))
await ctx.shutdown_task
# ensure auto launch is on the same page in regard to room activity.
with db_session:
room: Room = Room.get(id=ctx.room_id)
room.last_activity = datetime.datetime.utcnow() - datetime.timedelta(seconds=room.timeout + 60)
logging.info("Shutting down")
with Locker(room_id):
+1 -1
View File
@@ -5,5 +5,5 @@ Flask-Caching>=2.1.0
Flask-Compress>=1.14
Flask-Limiter>=3.5.0
bokeh>=3.1.1; python_version <= '3.8'
bokeh>=3.3.2; python_version >= '3.9'
bokeh>=3.2.2; python_version >= '3.9'
markupsafe>=2.1.3
+1 -1
View File
@@ -369,7 +369,7 @@ const setPresets = (optionsData, presetName) => {
break;
}
case 'named_range': {
case 'special_range': {
const selectElement = document.querySelector(`select[data-key='${option}']`);
const rangeElement = document.querySelector(`input[data-key='${option}']`);
const randomElement = document.querySelector(`.randomize-button[data-key='${option}']`);
+1 -1
View File
@@ -576,7 +576,7 @@ class GameSettings {
option = parseInt(option, 10);
let optionAcceptable = false;
if ((option >= setting.min) && (option <= setting.max)) {
if ((option > setting.min) && (option < setting.max)) {
optionAcceptable = true;
}
if (setting.hasOwnProperty('value_names') && Object.values(setting.value_names).includes(option)){
-10
View File
@@ -3,16 +3,6 @@
{% block head %}
<title>Multiworld {{ room.id|suuid }}</title>
{% if should_refresh %}<meta http-equiv="refresh" content="2">{% endif %}
<meta name="og:site_name" content="Archipelago">
<meta property="og:title" content="Multiworld {{ room.id|suuid }}">
<meta property="og:type" content="website" />
{% if room.seed.slots|length < 2 %}
<meta property="og:description" content="{{ room.seed.slots|length }} Player World
{% if room.last_port != -1 %}running on {{ config['HOST_ADDRESS'] }} with port {{ room.last_port }}{% endif %}">
{% else %}
<meta property="og:description" content="{{ room.seed.slots|length }} Players Multiworld
{% if room.last_port != -1 %}running on {{ config['HOST_ADDRESS'] }} with port {{ room.last_port }}{% endif %}">
{% endif %}
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/hostRoom.css") }}"/>
{% endblock %}
+1 -1
View File
@@ -1,6 +1,6 @@
{% block footer %}
<footer id="island-footer">
<div id="copyright-notice">Copyright 2024 Archipelago</div>
<div id="copyright-notice">Copyright 2023 Archipelago</div>
<div id="links">
<a href="/sitemap">Site Map</a>
-
+1 -1
View File
@@ -16,7 +16,7 @@
{% with messages = get_flashed_messages() %}
{% if messages %}
<div>
{% for message in messages | unique %}
{% for message in messages %}
<div class="user-message">{{ message }}</div>
{% endfor %}
</div>
+1 -1
View File
@@ -53,7 +53,7 @@
{% endif %}
{% if world.web.options_page is string %}
<span class="link-spacer">|</span>
<a href="{{ world.web.options_page }}">Options Page</a>
<a href="{{ world.web.settings_page }}">Options Page</a>
{% elif world.web.options_page %}
<span class="link-spacer">|</span>
<a href="{{ url_for("player_options", game=game_name) }}">Options Page</a>
+30 -52
View File
@@ -1,5 +1,4 @@
import datetime
import collections
from dataclasses import dataclass
from typing import Any, Callable, Dict, List, Optional, Set, Tuple
from uuid import UUID
@@ -9,7 +8,7 @@ from werkzeug.exceptions import abort
from MultiServer import Context, get_saving_second
from NetUtils import ClientStatus, Hint, NetworkItem, NetworkSlot, SlotType
from Utils import restricted_loads, KeyedDefaultDict
from Utils import restricted_loads
from . import app, cache
from .models import GameDataPackage, Room
@@ -63,18 +62,12 @@ class TrackerData:
self.location_name_to_id: Dict[str, Dict[str, int]] = {}
# Generate inverse lookup tables from data package, useful for trackers.
self.item_id_to_name: Dict[str, Dict[int, str]] = KeyedDefaultDict(lambda game_name: {
game_name: KeyedDefaultDict(lambda code: f"Unknown Game {game_name} - Item (ID: {code})")
})
self.location_id_to_name: Dict[str, Dict[int, str]] = KeyedDefaultDict(lambda game_name: {
game_name: KeyedDefaultDict(lambda code: f"Unknown Game {game_name} - Location (ID: {code})")
})
self.item_id_to_name: Dict[str, Dict[int, str]] = {}
self.location_id_to_name: Dict[str, Dict[int, str]] = {}
for game, game_package in self._multidata["datapackage"].items():
game_package = restricted_loads(GameDataPackage.get(checksum=game_package["checksum"]).data)
self.item_id_to_name[game] = KeyedDefaultDict(lambda code: f"Unknown Item (ID: {code})", {
id: name for name, id in game_package["item_name_to_id"].items()})
self.location_id_to_name[game] = KeyedDefaultDict(lambda code: f"Unknown Location (ID: {code})", {
id: name for name, id in game_package["location_name_to_id"].items()})
self.item_id_to_name[game] = {id: name for name, id in game_package["item_name_to_id"].items()}
self.location_id_to_name[game] = {id: name for name, id in game_package["location_name_to_id"].items()}
# Normal lookup tables as well.
self.item_name_to_id[game] = game_package["item_name_to_id"]
@@ -122,10 +115,10 @@ class TrackerData:
return self._multisave.get("received_items", {}).get((team, player, True), [])
@_cache_results
def get_player_inventory_counts(self, team: int, player: int) -> collections.Counter:
def get_player_inventory_counts(self, team: int, player: int) -> Dict[int, int]:
"""Retrieves a dictionary of all items received by their id and their received count."""
items = self.get_player_received_items(team, player)
inventory = collections.Counter()
inventory = {item: 0 for item in self.item_id_to_name[self.get_player_game(team, player)]}
for item in items:
inventory[item.item] += 1
@@ -156,15 +149,16 @@ class TrackerData:
"""Retrieves a dictionary of number of completed worlds per team."""
return {
team: sum(
self.get_player_client_status(team, player) == ClientStatus.CLIENT_GOAL for player in players
) for team, players in self.get_all_players().items()
self.get_player_client_status(team, player) == ClientStatus.CLIENT_GOAL
for player in players if self.get_slot_info(team, player).type == SlotType.player
) for team, players in self.get_team_players().items()
}
@_cache_results
def get_team_hints(self) -> Dict[int, Set[Hint]]:
"""Retrieves a dictionary of all hints per team."""
hints = {}
for team, players in self.get_all_slots().items():
for team, players in self.get_team_players().items():
hints[team] = set()
for player in players:
hints[team] |= self.get_player_hints(team, player)
@@ -176,7 +170,7 @@ class TrackerData:
"""Retrieves a dictionary of total player locations each team has."""
return {
team: sum(len(self.get_player_locations(team, player)) for player in players)
for team, players in self.get_all_players().items()
for team, players in self.get_team_players().items()
}
@_cache_results
@@ -184,30 +178,16 @@ class TrackerData:
"""Retrieves a dictionary of checked player locations each team has."""
return {
team: sum(len(self.get_player_checked_locations(team, player)) for player in players)
for team, players in self.get_all_players().items()
for team, players in self.get_team_players().items()
}
# TODO: Change this method to properly build for each team once teams are properly implemented, as they don't
# currently exist in multidata to easily look up, so these are all assuming only 1 team: Team #0
@_cache_results
def get_all_slots(self) -> Dict[int, List[int]]:
def get_team_players(self) -> Dict[int, List[int]]:
"""Retrieves a dictionary of all players ids on each team."""
return {
0: [
player for player, slot_info in self._multidata["slot_info"].items()
]
}
# TODO: Change this method to properly build for each team once teams are properly implemented, as they don't
# currently exist in multidata to easily look up, so these are all assuming only 1 team: Team #0
@_cache_results
def get_all_players(self) -> Dict[int, List[int]]:
"""Retrieves a dictionary of all player slot-type players ids on each team."""
return {
0: [
player for player, slot_info in self._multidata["slot_info"].items()
if self.get_slot_info(0, player).type == SlotType.player
]
0: [player for player, slot_info in self._multidata["slot_info"].items()]
}
@_cache_results
@@ -223,7 +203,7 @@ class TrackerData:
"""Retrieves a dictionary of all locations and their associated item metadata per player."""
return {
(team, player): self.get_player_locations(team, player)
for team, players in self.get_all_players().items() for player in players
for team, players in self.get_team_players().items() for player in players
}
@_cache_results
@@ -231,7 +211,7 @@ class TrackerData:
"""Retrieves a dictionary of games for each player."""
return {
(team, player): self.get_player_game(team, player)
for team, players in self.get_all_slots().items() for player in players
for team, players in self.get_team_players().items() for player in players
}
@_cache_results
@@ -239,7 +219,7 @@ class TrackerData:
"""Retrieves a dictionary of all locations complete per player."""
return {
(team, player): len(self.get_player_checked_locations(team, player))
for team, players in self.get_all_players().items() for player in players
for team, players in self.get_team_players().items() for player in players
}
@_cache_results
@@ -247,14 +227,14 @@ class TrackerData:
"""Retrieves a dictionary of all ClientStatus values per player."""
return {
(team, player): self.get_player_client_status(team, player)
for team, players in self.get_all_players().items() for player in players
for team, players in self.get_team_players().items() for player in players
}
@_cache_results
def get_room_long_player_names(self) -> Dict[TeamPlayer, str]:
"""Retrieves a dictionary of names with aliases for each player."""
long_player_names = {}
for team, players in self.get_all_slots().items():
for team, players in self.get_team_players().items():
for player in players:
alias = self.get_player_alias(team, player)
if alias:
@@ -390,8 +370,7 @@ def render_generic_multiworld_tracker(tracker_data: TrackerData, enabled_tracker
enabled_trackers=enabled_trackers,
current_tracker="Generic",
room=tracker_data.room,
all_slots=tracker_data.get_all_slots(),
room_players=tracker_data.get_all_players(),
room_players=tracker_data.get_team_players(),
locations=tracker_data.get_room_locations(),
locations_complete=tracker_data.get_room_locations_complete(),
total_team_locations=tracker_data.get_team_locations_total_count(),
@@ -410,6 +389,7 @@ def render_generic_multiworld_tracker(tracker_data: TrackerData, enabled_tracker
# TODO: This is a temporary solution until a proper Tracker API can be implemented for tracker templates and data to
# live in their respective world folders.
import collections
from worlds import network_data_package
@@ -420,7 +400,7 @@ if "Factorio" in network_data_package["games"]:
(team, player): {
tracker_data.item_id_to_name["Factorio"][item_id]: count
for item_id, count in tracker_data.get_player_inventory_counts(team, player).items()
} for team, players in tracker_data.get_all_slots().items() for player in players
} for team, players in tracker_data.get_team_players().items() for player in players
if tracker_data.get_player_game(team, player) == "Factorio"
}
@@ -429,8 +409,7 @@ if "Factorio" in network_data_package["games"]:
enabled_trackers=enabled_trackers,
current_tracker="Factorio",
room=tracker_data.room,
all_slots=tracker_data.get_all_slots(),
room_players=tracker_data.get_all_players(),
room_players=tracker_data.get_team_players(),
locations=tracker_data.get_room_locations(),
locations_complete=tracker_data.get_room_locations_complete(),
total_team_locations=tracker_data.get_team_locations_total_count(),
@@ -568,7 +547,7 @@ if "A Link to the Past" in network_data_package["games"]:
if area_name != "Total" else tracker_data._multidata["checks_in_area"][player]["Total"]
for area_name in ordered_areas
}
for team, players in tracker_data.get_all_slots().items()
for team, players in tracker_data.get_team_players().items()
for player in players
if tracker_data.get_slot_info(team, player).type != SlotType.group and
tracker_data.get_slot_info(team, player).game == "A Link to the Past"
@@ -606,7 +585,7 @@ if "A Link to the Past" in network_data_package["games"]:
player_location_to_area = {
(team, player): _get_location_table(tracker_data._multidata["checks_in_area"][player])
for team, players in tracker_data.get_all_slots().items()
for team, players in tracker_data.get_team_players().items()
for player in players
if tracker_data.get_slot_info(team, player).type != SlotType.group and
tracker_data.get_slot_info(team, player).game == "A Link to the Past"
@@ -614,15 +593,15 @@ if "A Link to the Past" in network_data_package["games"]:
checks_done: Dict[TeamPlayer, Dict[str: int]] = {
(team, player): {location_name: 0 for location_name in default_locations}
for team, players in tracker_data.get_all_slots().items()
for team, players in tracker_data.get_team_players().items()
for player in players
if tracker_data.get_slot_info(team, player).type != SlotType.group and
tracker_data.get_slot_info(team, player).game == "A Link to the Past"
}
inventories: Dict[TeamPlayer, Dict[int, int]] = {}
player_big_key_locations = {(player): set() for player in tracker_data.get_all_slots()[0]}
player_small_key_locations = {player: set() for player in tracker_data.get_all_slots()[0]}
player_big_key_locations = {(player): set() for player in tracker_data.get_team_players()[0]}
player_small_key_locations = {player: set() for player in tracker_data.get_team_players()[0]}
group_big_key_locations = set()
group_key_locations = set()
@@ -660,8 +639,7 @@ if "A Link to the Past" in network_data_package["games"]:
enabled_trackers=enabled_trackers,
current_tracker="A Link to the Past",
room=tracker_data.room,
all_slots=tracker_data.get_all_slots(),
room_players=tracker_data.get_all_players(),
room_players=tracker_data.get_team_players(),
locations=tracker_data.get_room_locations(),
locations_complete=tracker_data.get_room_locations_complete(),
total_team_locations=tracker_data.get_team_locations_total_count(),
+28 -61
View File
@@ -11,46 +11,17 @@ from flask import request, flash, redirect, url_for, session, render_template
from markupsafe import Markup
from pony.orm import commit, flush, select, rollback
from pony.orm.core import TransactionIntegrityError
import schema
import MultiServer
from NetUtils import SlotType
from Utils import VersionException, __version__
from worlds import GamesPackage
from worlds.Files import AutoPatchRegister
from worlds.AutoWorld import data_package_checksum
from . import app
from .models import Seed, Room, Slot, GameDataPackage
banned_extensions = (".sfc", ".z64", ".n64", ".nes", ".smc", ".sms", ".gb", ".gbc", ".gba")
allowed_options_extensions = (".yaml", ".json", ".yml", ".txt", ".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:
return filename.endswith(allowed_options_extensions)
def allowed_generation(filename: str) -> bool:
return filename.endswith(allowed_generation_extensions)
def banned_file(filename: str) -> bool:
return filename.endswith(banned_extensions)
banned_zip_contents = (".sfc", ".z64", ".n64", ".sms", ".gb")
def process_multidata(compressed_multidata, files={}):
game_data: GamesPackage
decompressed_multidata = MultiServer.Context.decompress(compressed_multidata)
slots: typing.Set[Slot] = set()
@@ -59,19 +30,11 @@ def process_multidata(compressed_multidata, files={}):
game_data_packages: typing.List[GameDataPackage] = []
for game, game_data in decompressed_multidata["datapackage"].items():
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"],
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] = {
"version": game_data.get("version", 0),
"checksum": game_data["checksum"],
"checksum": game_data["checksum"]
}
try:
commit() # commit game data package
@@ -86,21 +49,20 @@ def process_multidata(compressed_multidata, files={}):
if slot_info.type == SlotType.group:
continue
slots.add(Slot(data=files.get(slot, None),
player_name=slot_info.name,
player_id=slot,
game=slot_info.game))
player_name=slot_info.name,
player_id=slot,
game=slot_info.game))
flush() # commit slots
compressed_multidata = compressed_multidata[0:1] + zlib.compress(pickle.dumps(decompressed_multidata), 9)
return slots, compressed_multidata
def upload_zip_to_db(zfile: zipfile.ZipFile, owner=None, meta={"race": False}, sid=None):
if not owner:
owner = session["_id"]
infolist = zfile.infolist()
if all(allowed_options(file.filename) or file.is_dir() for file in infolist):
flash(Markup("Error: Your .zip file only contains options files. "
if all(file.filename.endswith((".yaml", ".yml")) or file.is_dir() for file in infolist):
flash(Markup("Error: Your .zip file only contains .yaml files. "
'Did you mean to <a href="/generate">generate a game</a>?'))
return
@@ -111,7 +73,7 @@ def upload_zip_to_db(zfile: zipfile.ZipFile, owner=None, meta={"race": False}, s
# Load files.
for file in infolist:
handler = AutoPatchRegister.get_handler(file.filename)
if banned_file(file.filename):
if file.filename.endswith(banned_zip_contents):
return "Uploaded data contained a rom file, which is likely to contain copyrighted material. " \
"Your file was deleted."
@@ -174,34 +136,35 @@ def upload_zip_to_db(zfile: zipfile.ZipFile, owner=None, meta={"race": False}, s
flash("No multidata was found in the zip file, which is required.")
@app.route("/uploads", methods=["GET", "POST"])
@app.route('/uploads', methods=['GET', 'POST'])
def uploads():
if request.method == "POST":
# check if the POST request has a file part.
if "file" not in request.files:
flash("No file part in POST request.")
if request.method == 'POST':
# check if the post request has the file part
if 'file' not in request.files:
flash('No file part')
else:
uploaded_file = request.files["file"]
# If the user does not select file, the browser will still submit an empty string without a file name.
if uploaded_file.filename == "":
flash("No selected file.")
elif uploaded_file and allowed_generation(uploaded_file.filename):
if zipfile.is_zipfile(uploaded_file):
with zipfile.ZipFile(uploaded_file, "r") as zfile:
file = request.files['file']
# if user does not select file, browser also
# submit an empty part without filename
if file.filename == '':
flash('No selected file')
elif file and allowed_file(file.filename):
if zipfile.is_zipfile(file):
with zipfile.ZipFile(file, 'r') as zfile:
try:
res = upload_zip_to_db(zfile)
except VersionException:
flash(f"Could not load multidata. Wrong Version detected.")
else:
if res is str:
if type(res) == str:
return res
elif res:
return redirect(url_for("view_seed", seed=res.id))
else:
uploaded_file.seek(0) # offset from is_zipfile check
file.seek(0) # offset from is_zipfile check
# noinspection PyBroadException
try:
multidata = uploaded_file.read()
multidata = file.read()
slots, multidata = process_multidata(multidata)
except Exception as e:
flash(f"Could not load multidata. File may be corrupted or incompatible. ({e})")
@@ -219,3 +182,7 @@ def user_content():
rooms = select(room for room in Room if room.owner == session["_id"])
seeds = select(seed for seed in Seed if seed.owner == session["_id"])
return render_template("userContent.html", rooms=rooms, seeds=seeds)
def allowed_file(filename):
return filename.endswith(('.archipelago', ".zip"))
+3 -2
View File
@@ -13,6 +13,7 @@ from typing import List
import Utils
from Utils import async_start
from worlds import lookup_any_location_id_to_name
from CommonClient import CommonContext, server_loop, gui_enabled, console_loop, ClientCommandProcessor, logger, \
get_base_parser
@@ -152,7 +153,7 @@ def get_payload(ctx: ZeldaContext):
def reconcile_shops(ctx: ZeldaContext):
checked_location_names = [ctx.location_names[location] for location in ctx.checked_locations]
checked_location_names = [lookup_any_location_id_to_name[location] for location in ctx.checked_locations]
shops = [location for location in checked_location_names if "Shop" in location]
left_slots = [shop for shop in shops if "Left" in shop]
middle_slots = [shop for shop in shops if "Middle" in shop]
@@ -190,7 +191,7 @@ async def parse_locations(locations_array, ctx: ZeldaContext, force: bool, zone=
locations_checked = []
location = None
for location in ctx.missing_locations:
location_name = ctx.location_names[location]
location_name = lookup_any_location_id_to_name[location]
if location_name in Locations.overworld_locations and zone == "overworld":
status = locations_array[Locations.major_location_offsets[location_name]]
+500 -5
View File
@@ -1,10 +1,505 @@
import ModuleUpdate
ModuleUpdate.update()
import asyncio
import base64
import platform
from typing import Any, ClassVar, Coroutine, Dict, List, Optional, Protocol, Tuple, Type, 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 # type: ignore
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: Type[ClientCommandProcessor] = 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[f"zillion-{self.auth}-doors"]
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__":
Utils.init_logging("ZillionClient", exception_logger="Client")
launch()
colorama.init()
asyncio.run(main())
colorama.deinit()
+1 -2
View File
@@ -456,7 +456,6 @@ function send_receive ()
failed_guard_response = response
end
else
if type(response) ~= "string" then response = "Unknown error" end
res[i] = {type = "ERROR", err = response}
end
end
@@ -586,7 +585,7 @@ else
-- misaligned, so for GB and GBC we explicitly set the callback on
-- vblank instead.
-- https://github.com/TASEmulators/BizHawk/issues/3711
if emu.getsystemid() == "GB" or emu.getsystemid() == "GBC" or emu.getsystemid() == "SGB" then
if emu.getsystemid() == "GB" or emu.getsystemid() == "GBC" then
event.onmemoryexecute(tick, 0x40, "tick", "System Bus")
else
event.onframeend(tick)
+1 -1
View File
@@ -7,7 +7,7 @@ Contributions are welcome. We have a few requests for new contributors:
* **Ensure that critical changes are covered by tests.**
It is strongly recommended that unit tests are used to avoid regression and to ensure everything is still working.
If you wish to contribute by adding a new game, please take a look at the [logic unit test documentation](/docs/tests.md).
If you wish to contribute by adding a new game, please take a look at the [logic unit test documentation](/docs/world%20api.md#tests).
If you wish to contribute to the website, please take a look at [these tests](/test/webhost).
* **Do not introduce unit test failures/regressions.**
+6 -7
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}`.
| Name | Type | Notes |
|----------------------------------|-------------------------------|-------------------------------------------------------|
| 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. |
| 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. |
| Name | Type | Notes |
|------------------------------|-------------------------------|---------------------------------------------------|
| 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. |
| item_name_groups_{game_name} | dict\[str, list\[str\]\] | item_name_groups belonging to the requested game. |
| client_status_{team}_{slot} | [ClientStatus](#ClientStatus) | The current game status of the requested player. |
### 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.
-90
View File
@@ -1,90 +0,0 @@
# Archipelago Unit Testing API
This document covers some of the generic tests available using Archipelago's unit testing system, as well as some basic
steps on how to write your own.
## Generic Tests
Some generic tests are run on every World to ensure basic functionality with default options. These basic tests can be
found in the [general test directory](/test/general).
## Defining World Tests
In order to run tests from your world, you will need to create a `test` package within your world package. This can be
done by creating a `test` directory with a file named `__init__.py` inside it inside your world. By convention, a base
for your world tests can be created in this file that you can then import into other modules.
### WorldTestBase
In order to test basic functionality of varying options, as well as to test specific edge cases or that certain
interactions in the world interact as expected, you will want to use the [WorldTestBase](/test/bases.py). This class
comes with the basics for test setup as well as a few preloaded tests that most worlds might want to check on varying
options combinations.
Example `/worlds/<my_game>/test/__init__.py`:
```python
from test.bases import WorldTestBase
class MyGameTestBase(WorldTestBase):
game = "My Game"
```
The basic tests that WorldTestBase comes with include `test_all_state_can_reach_everything`,
`test_empty_state_can_reach_something`, and `test_fill`. These test that with all collected items everything is
reachable, with no collected items at least something is reachable, and that a valid multiworld can be completed with
all steps being called, respectively.
### Writing Tests
#### Using WorldTestBase
Adding runs for the basic tests for a different option combination is as easy as making a new module in the test
package, creating a class that inherits from your game's TestBase, and defining the options in a dict as a field on the
class. The new module should be named `test_<something>.py` and have at least one class inheriting from the base, or
define its own testing methods. Newly defined test methods should follow standard PEP8 snake_case format and also start
with `test_`.
Example `/worlds/<my_game>/test/test_chest_access.py`:
```python
from . import MyGameTestBase
class TestChestAccess(MyGameTestBase):
options = {
"difficulty": "easy",
"final_boss_hp": 4000,
}
def test_sword_chests(self) -> None:
"""Test locations that require a sword"""
locations = ["Chest1", "Chest2"]
items = [["Sword"]]
# This tests that the provided locations aren't accessible without the provided items, but can be accessed once
# the items are obtained.
# This will also check that any locations not provided don't have the same dependency requirement.
# Optionally, passing only_check_listed=True to the method will only check the locations provided.
self.assertAccessDependency(locations, items)
```
When tests are run, this class will create a multiworld with a single player having the provided options, and run the
generic tests, as well as the new custom test. Each test method definition will create its own separate solo multiworld
that will be cleaned up after. If you don't want to run the generic tests on a base, `run_default_tests` can be
overridden. For more information on what methods are available to your class, check the
[WorldTestBase definition](/test/bases.py#L104).
#### Alternatives to WorldTestBase
Unit tests can also be created using [TestBase](/test/bases.py#L14) or
[unittest.TestCase](https://docs.python.org/3/library/unittest.html#unittest.TestCase) depending on your use case. These
may be useful for generating a multiworld under very specific constraints without using the generic world setup, or for
testing portions of your code that can be tested without relying on a multiworld to be created first.
## Running Tests
In PyCharm, running all tests can be done by right-clicking the root `test` directory and selecting `run Python tests`.
If you do not have pytest installed, you may get import failures. To solve this, edit the run configuration, and set the
working directory of the run to the Archipelago directory. If you only want to run your world's defined tests, repeat
the steps for the test directory within your world.
+2 -4
View File
@@ -870,7 +870,7 @@ TestBase, and can then define options to test in the class body, and run tests i
Example `__init__.py`
```python
from test.bases import WorldTestBase
from test.test_base import WorldTestBase
class MyGameTestBase(WorldTestBase):
@@ -879,7 +879,7 @@ class MyGameTestBase(WorldTestBase):
Next using the rules defined in the above `set_rules` we can test that the chests have the correct access rules.
Example `test_chest_access.py`
Example `testChestAccess.py`
```python
from . import MyGameTestBase
@@ -899,5 +899,3 @@ class TestChestAccess(MyGameTestBase):
# this will test that chests 3-5 can't be accessed without any weapon, but can be with just one of them.
self.assertAccessDependency(locations, items)
```
For more information on tests check the [tests doc](tests.md).
+3 -3
View File
@@ -1,13 +1,13 @@
colorama>=0.4.5
websockets>=12.0
websockets>=11.0.3
PyYAML>=6.0.1
jellyfish>=1.0.3
jinja2>=3.1.2
schema>=0.7.5
kivy>=2.2.1
kivy>=2.2.0
bsdiff4>=1.2.4
platformdirs>=4.0.0
certifi>=2023.11.17
cython>=3.0.6
cython>=3.0.5
cymem>=2.0.8
orjson>=3.9.10
+1 -35
View File
@@ -1,8 +1,5 @@
import unittest
from Fill import distribute_items_restrictive
from worlds.AutoWorld import AutoWorldRegister, call_all
from . import setup_solo_multiworld
from worlds.AutoWorld import AutoWorldRegister
class TestIDs(unittest.TestCase):
@@ -69,34 +66,3 @@ class TestIDs(unittest.TestCase):
for gamename, world_type in AutoWorldRegister.world_types.items():
with self.subTest(game=gamename):
self.assertEqual(len(world_type.location_id_to_name), len(world_type.location_name_to_id))
def test_postgen_datapackage(self):
"""Generates a solo multiworld and checks that the datapackage is still valid"""
for gamename, world_type in AutoWorldRegister.world_types.items():
with self.subTest(game=gamename):
multiworld = setup_solo_multiworld(world_type)
distribute_items_restrictive(multiworld)
call_all(multiworld, "post_fill")
datapackage = world_type.get_data_package_data()
for item_group, item_names in datapackage["item_name_groups"].items():
self.assertIsInstance(item_group, str,
f"item_name_group names should be strings: {item_group}")
for item_name in item_names:
self.assertIsInstance(item_name, str,
f"{item_name}, in group {item_group} is not a string")
for loc_group, loc_names in datapackage["location_name_groups"].items():
self.assertIsInstance(loc_group, str,
f"location_name_group names should be strings: {loc_group}")
for loc_name in loc_names:
self.assertIsInstance(loc_name, str,
f"{loc_name}, in group {loc_group} is not a string")
for item_name, item_id in datapackage["item_name_to_id"].items():
self.assertIsInstance(item_name, str,
f"{item_name} is not a valid item name for item_name_to_id")
self.assertIsInstance(item_id, int,
f"{item_id} for {item_name} should be an int")
for loc_name, loc_id in datapackage["location_name_to_id"].items():
self.assertIsInstance(loc_name, str,
f"{loc_name} is not a valid item name for location_name_to_id")
self.assertIsInstance(loc_id, int,
f"{loc_id} for {loc_name} should be an int")
-4
View File
@@ -77,10 +77,6 @@ class AutoWorldRegister(type):
# create missing options_dataclass from legacy option_definitions
# TODO - remove this once all worlds use options dataclasses
if "options_dataclass" not in dct and "option_definitions" in dct:
# TODO - switch to deprecate after a version
if __debug__:
from warnings import warn
warn("Assigning options through option_definitions is now deprecated. Use options_dataclass instead.")
dct["options_dataclass"] = make_dataclass(f"{name}Options", dct["option_definitions"].items(),
bases=(PerGameCommonOptions,))
+1 -1
View File
@@ -97,7 +97,7 @@ async def connect(ctx: BizHawkContext) -> bool:
for port in ports:
try:
ctx.streams = await asyncio.open_connection("127.0.0.1", port)
ctx.streams = await asyncio.open_connection("localhost", port)
ctx.connection_status = ConnectionStatus.TENTATIVE
ctx._port = port
return True
+10 -21
View File
@@ -208,30 +208,19 @@ async def _run_game(rom: str):
if auto_start is True:
emuhawk_path = Utils.get_settings().bizhawkclient_options.emuhawk_path
subprocess.Popen(
[
emuhawk_path,
f"--lua={Utils.local_path('data', 'lua', 'connector_bizhawk_generic.lua')}",
os.path.realpath(rom),
],
cwd=Utils.local_path("."),
stdin=subprocess.DEVNULL,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
subprocess.Popen([emuhawk_path, "--lua=data/lua/connector_bizhawk_generic.lua", os.path.realpath(rom)],
cwd=Utils.local_path("."),
stdin=subprocess.DEVNULL,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL)
elif isinstance(auto_start, str):
import shlex
subprocess.Popen(
[
*shlex.split(auto_start),
os.path.realpath(rom)
],
cwd=Utils.local_path("."),
stdin=subprocess.DEVNULL,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL
)
subprocess.Popen([*shlex.split(auto_start), os.path.realpath(rom)],
cwd=Utils.local_path("."),
stdin=subprocess.DEVNULL,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL)
async def _patch_and_run_game(patch_file: str):
+2 -1
View File
@@ -10,7 +10,8 @@ As we are using BizHawk, this guide is only applicable to Windows and Linux syst
- Version 2.3.1 and later are supported. Version 2.7 is recommended for stability.
- Detailed installation instructions for BizHawk can be found at the above link.
- Windows users must run the prereq installer first, which can also be found at the above link.
- The built-in Archipelago client, which can be installed [here](https://github.com/ArchipelagoMW/Archipelago/releases).
- The built-in Archipelago client, which can be installed [here](https://github.com/ArchipelagoMW/Archipelago/releases)
(select `Adventure Client` during installation).
- An Adventure NTSC ROM file. The Archipelago community cannot provide these.
## Configuring BizHawk
+1 -1
View File
@@ -264,7 +264,7 @@ def fill_dungeons_restrictive(multiworld: MultiWorld):
if loc in all_state_base.events:
all_state_base.events.remove(loc)
fill_restrictive(multiworld, all_state_base, locations, in_dungeon_items, True, True, allow_excluded=True,
fill_restrictive(multiworld, all_state_base, locations, in_dungeon_items, True, True,
name="LttP Dungeon Items")
+2
View File
@@ -682,6 +682,8 @@ def get_pool_core(world, player: int):
key_location = world.random.choice(key_locations)
place_item(key_location, "Small Key (Universal)")
pool = pool[:-3]
if world.key_drop_shuffle[player]:
pass # pool.extend([item_to_place] * (len(key_drop_data) - 1))
return (pool, placed_items, precollected_items, clock_mode, treasure_hunt_count, treasure_hunt_icon,
additional_pieces_to_place)
+2 -7
View File
@@ -1,7 +1,8 @@
import typing
from BaseClasses import MultiWorld
from Options import Choice, Range, Option, Toggle, DefaultOnToggle, DeathLink, StartInventoryPool, PlandoBosses
from Options import Choice, Range, Option, Toggle, DefaultOnToggle, DeathLink, AllowCollect, StartInventoryPool, \
PlandoBosses
class Logic(Choice):
@@ -426,12 +427,6 @@ class BeemizerTrapChance(BeemizerRange):
display_name = "Beemizer Trap Chance"
class AllowCollect(Toggle):
"""Allows for !collect / co-op to auto-open chests containing items for other players.
Off by default, because it currently crashes on real hardware."""
display_name = "Allow Collection of checks for other players"
alttp_options: typing.Dict[str, type(Option)] = {
"crystals_needed_for_gt": CrystalsTower,
"crystals_needed_for_ganon": CrystalsGanon,
+1 -2
View File
@@ -136,8 +136,7 @@ def mirrorless_path_to_castle_courtyard(world, player):
def set_defeat_dungeon_boss_rule(location):
# Lambda required to defer evaluation of dungeon.boss since it will change later if boss shuffle is used
add_rule(location, lambda state: location.parent_region.dungeon.boss.can_defeat(state))
set_rule(location, lambda state: location.parent_region.dungeon.boss.can_defeat(state))
def set_always_allow(spot, rule):
spot.always_allow = rule
+6 -11
View File
@@ -289,17 +289,12 @@ class ALTTPWorld(World):
self.waterfall_fairy_bottle_fill = self.random.choice(bottle_options)
self.pyramid_fairy_bottle_fill = self.random.choice(bottle_options)
if multiworld.mode[player] == 'standard':
if multiworld.smallkey_shuffle[player]:
if (multiworld.smallkey_shuffle[player] not in
(smallkey_shuffle.option_universal, smallkey_shuffle.option_own_dungeons,
smallkey_shuffle.option_start_with)):
self.multiworld.local_early_items[self.player]["Small Key (Hyrule Castle)"] = 1
self.multiworld.local_items[self.player].value.add("Small Key (Hyrule Castle)")
self.multiworld.non_local_items[self.player].value.discard("Small Key (Hyrule Castle)")
if multiworld.bigkey_shuffle[player]:
self.multiworld.local_items[self.player].value.add("Big Key (Hyrule Castle)")
self.multiworld.non_local_items[self.player].value.discard("Big Key (Hyrule Castle)")
if multiworld.mode[player] == 'standard' \
and multiworld.smallkey_shuffle[player] \
and multiworld.smallkey_shuffle[player] != smallkey_shuffle.option_universal \
and multiworld.smallkey_shuffle[player] != smallkey_shuffle.option_own_dungeons \
and multiworld.smallkey_shuffle[player] != smallkey_shuffle.option_start_with:
self.multiworld.local_early_items[self.player]["Small Key (Hyrule Castle)"] = 1
# system for sharing ER layouts
self.er_seed = str(multiworld.random.randint(0, 2 ** 64))
+6 -6
View File
@@ -2,7 +2,8 @@
## Required Software
- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases).
- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases). Make sure to check the box for
`SNI Client - A Link to the Past Patch Setup`
- [SNI](https://github.com/alttpo/sni/releases). This is automatically included with your Archipelago installation above.
- SNI is not compatible with (Q)Usb2Snes.
- Hardware or software capable of loading and playing SNES ROM files
@@ -17,12 +18,11 @@ but it is not supported.**
## Installation Procedures
1. Download and install [Archipelago](<https://github.com/ArchipelagoMW/Archipelago/releases/latest>). **The installer
file is located in the assets section at the bottom of the version information.**
2. The first time you do local generation or patch your game, you will be asked to locate your base ROM file.
This is your Japanese Link to the Past ROM file. This only needs to be done once.
1. Download and install SNIClient from the link above, making sure to install the most recent version.
**The installer file is located in the assets section at the bottom of the version information**.
- During setup, you will be asked to locate your base ROM file. This is your Japanese Link to the Past ROM file.
3. If you are using an emulator, you should assign your Lua capable emulator as your default program for launching ROM
2. If you are using an emulator, you should assign your Lua capable emulator as your default program for launching ROM
files.
1. Extract your emulator's folder to your Desktop, or somewhere you will remember.
2. Right-click on a ROM file and select **Open with...**
+1
View File
@@ -5,6 +5,7 @@
- ChecksFinder from
the [Github releases Page for the game](https://github.com/jonloveslegos/ChecksFinder/releases) (latest version)
- Archipelago from the [Archipelago Releases Page](https://github.com/ArchipelagoMW/Archipelago/releases)
- (select `ChecksFinder Client` during installation.)
## Configuring your YAML file
+1 -14
View File
@@ -21,20 +21,7 @@ This client has only been tested with the Official Steam version of the game at
## Downpatching Dark Souls III
To downpatch DS3 for use with Archipelago, use the following instructions from the speedsouls wiki database.
1. Launch Steam (in online mode).
2. Press the Windows Key + R. This will open the Run window.
3. Open the Steam console by typing the following string: steam://open/console , Steam should now open in Console Mode.
4. Insert the string of the depot you wish to download. For the AP supported v1.15, you will want to use: download_depot 374320 374321 4471176929659548333.
5. Steam will now download the depot. Note: There is no progress bar of the download in Steam, but it is still downloading in the background.
6. Turn off auto-updates in Steam by right-clicking Dark Souls III in your library > Properties > Updates > set "Automatic Updates" to "Only update this game when I launch it" (or change the value for AutoUpdateBehavior to 1 in "\Steam\steamapps\appmanifest_374320.acf").
7. Back up your existing game folder in "\Steam\steamapps\common\DARK SOULS III".
8. Return back to Steam console. Once the download is complete, it should say so along with the temporary local directory in which the depot has been stored. This is usually something like "\Steam\steamapps\content\app_XXXXXX\depot_XXXXXX". Back up this game folder as well.
9. Delete your existing game folder in "\Steam\steamapps\common\DARK SOULS III", then replace it with your game folder in "\Steam\steamapps\content\app_XXXXXX\depot_XXXXXX".
10. Back up and delete your save file "DS30000.sl2" in AppData. AppData is hidden by default. To locate it, press Windows Key + R, type %appdata% and hit enter or: open File Explorer > View > Hidden Items and follow "C:\Users\your username\AppData\Roaming\DarkSoulsIII\numbers".
11. If you did all these steps correctly, you should be able to confirm your game version in the upper left corner after launching Dark Souls III.
Follow instructions from the [speedsouls wiki](https://wiki.speedsouls.com/darksouls3:Downpatching) to download version 1.15. Your download command, including the correct depot and manifest ids, will be "download_depot 374320 374321 4471176929659548333"
## Installing the Archipelago mod
+4 -5
View File
@@ -2,7 +2,7 @@
## Required Software
- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases).
- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases). Make sure to check the box for `SNI Client - Donkey Kong Country 3 Patch Setup`
- Hardware or software capable of loading and playing SNES ROM files
@@ -23,10 +23,9 @@
### Windows Setup
1. Download and install [Archipelago](<https://github.com/ArchipelagoMW/Archipelago/releases/latest>). **The installer
file is located in the assets section at the bottom of the version information.**
2. The first time you do local generation or patch your game, you will be asked to locate your base ROM file.
This is your Donkey Kong Country 3 ROM file. This only needs to be done once.
1. During the installation of Archipelago, you will have been asked to install the SNI Client. If you did not do this,
or you are on an older version, you may run the installer again to install the SNI Client.
2. During setup, you will be asked to locate your base ROM file. This is your Donkey Kong Country 3 ROM file.
3. If you are using an emulator, you should assign your Lua capable emulator as your default program for launching ROM
files.
1. Extract your emulator's folder to your Desktop, or somewhere you will remember.
+8 -10
View File
@@ -5,7 +5,7 @@ import os
import shutil
import threading
import zipfile
from typing import Optional, TYPE_CHECKING, Any, List, Callable, Tuple, Union
from typing import Optional, TYPE_CHECKING, Any, List, Callable, Tuple
import jinja2
@@ -63,7 +63,7 @@ recipe_time_ranges = {
class FactorioModFile(worlds.Files.APContainer):
game = "Factorio"
compression_method = zipfile.ZIP_DEFLATED # Factorio can't load LZMA archives
writing_tasks: List[Callable[[], Tuple[str, Union[str, bytes]]]]
writing_tasks: List[Callable[[], Tuple[str, str]]]
def __init__(self, *args: Any, **kwargs: Any):
super().__init__(*args, **kwargs)
@@ -164,7 +164,9 @@ def generate_mod(world: "Factorio", output_directory: str):
template_data["free_sample_blacklist"].update({item: 1 for item in multiworld.free_sample_blacklist[player].value})
template_data["free_sample_blacklist"].update({item: 0 for item in multiworld.free_sample_whitelist[player].value})
zf_path = os.path.join(output_directory, versioned_mod_name + ".zip")
mod_dir = os.path.join(output_directory, versioned_mod_name)
zf_path = os.path.join(mod_dir + ".zip")
mod = FactorioModFile(zf_path, player=player, player_name=multiworld.player_name[player])
if world.zip_path:
@@ -175,13 +177,7 @@ def generate_mod(world: "Factorio", output_directory: str):
mod.writing_tasks.append(lambda arcpath=versioned_mod_name+"/"+path_part, content=zf.read(file):
(arcpath, content))
else:
basepath = os.path.join(os.path.dirname(__file__), "data", "mod")
for dirpath, dirnames, filenames in os.walk(basepath):
base_arc_path = (versioned_mod_name+"/"+os.path.relpath(dirpath, basepath)).rstrip("/.\\")
for filename in filenames:
mod.writing_tasks.append(lambda arcpath=base_arc_path+"/"+filename,
file_path=os.path.join(dirpath, filename):
(arcpath, open(file_path, "rb").read()))
shutil.copytree(os.path.join(os.path.dirname(__file__), "data", "mod"), mod_dir, dirs_exist_ok=True)
mod.writing_tasks.append(lambda: (versioned_mod_name + "/data.lua",
data_template.render(**template_data)))
@@ -201,3 +197,5 @@ def generate_mod(world: "Factorio", output_directory: str):
# write the mod file
mod.write()
# clean up
shutil.rmtree(mod_dir)
+6 -4
View File
@@ -2,8 +2,8 @@ from __future__ import annotations
import typing
import datetime
from Options import Choice, OptionDict, OptionSet, Option, DefaultOnToggle, Range, DeathLink, Toggle, \
StartInventoryPool
from Options import Choice, OptionDict, OptionSet, ItemDict, Option, DefaultOnToggle, Range, DeathLink, AllowCollect, \
Toggle, StartInventoryPool
from schema import Schema, Optional, And, Or
# schema helpers
@@ -207,10 +207,11 @@ class RecipeIngredientsOffset(Range):
range_end = 5
class FactorioStartItems(OptionDict):
class FactorioStartItems(ItemDict):
"""Mapping of Factorio internal item-name to amount granted on start."""
display_name = "Starting Items"
default = {"burner-mining-drill": 4, "stone-furnace": 4, "raw-fish": 50}
verify_item_name = False
default = {"burner-mining-drill": 19, "stone-furnace": 19}
class FactorioFreeSampleBlacklist(OptionSet):
@@ -453,6 +454,7 @@ factorio_options: typing.Dict[str, type(Option)] = {
"evolution_traps": EvolutionTrapCount,
"evolution_trap_increase": EvolutionTrapIncrease,
"death_link": DeathLink,
"allow_collect": AllowCollect,
"energy_link": EnergyLink,
"start_inventory_from_pool": StartInventoryPool,
}
+1 -2
View File
@@ -246,8 +246,7 @@ class Factorio(World):
location.access_rule = lambda state, ingredient=ingredient, custom_recipe=custom_recipe: \
(ingredient not in technology_table or state.has(ingredient, player)) and \
all(state.has(technology.name, player) for sub_ingredient in custom_recipe.ingredients
for technology in required_technologies[sub_ingredient]) and \
all(state.has(technology.name, player) for technology in required_technologies[custom_recipe.crafting_machine])
for technology in required_technologies[sub_ingredient])
else:
location.access_rule = lambda state, ingredient=ingredient: \
all(state.has(technology.name, player) for technology in required_technologies[ingredient])
+42 -4
View File
@@ -11,6 +11,7 @@ TRAP_EVO_FACTOR = {{ evolution_trap_increase }} / 100
MAX_SCIENCE_PACK = {{ max_science_pack }}
GOAL = {{ goal }}
ARCHIPELAGO_DEATH_LINK_SETTING = "archipelago-death-link-{{ slot_player }}-{{ seed_name }}"
ARCHIPELAGO_ALLOW_COLLECT_SETTING = "archipelago-allow-collect-{{ slot_player }}-{{ seed_name }}"
ENERGY_INCREMENT = {{ energy_link * 10000000 }}
ENERGY_LINK_EFFICIENCY = 0.75
@@ -20,6 +21,12 @@ else
DEATH_LINK = 0
end
if settings.global[ARCHIPELAGO_ALLOW_COLLECT_SETTING].value then
ALLOW_COLLECT = 1
else
ALLOW_COLLECT = 0
end
CURRENTLY_DEATH_LOCK = 0
{% if chunk_shuffle %}
@@ -257,6 +264,26 @@ function on_runtime_mod_setting_changed(event)
dumpInfo(force)
end
end
if event.setting == ARCHIPELAGO_ALLOW_COLLECT_SETTING then
local force = game.forces["player"]
if global.received_tech == nil then
global.received_tech = {}
end
if settings.global[ARCHIPELAGO_ALLOW_COLLECT_SETTING].value then
ALLOW_COLLECT = 1
for item_name, _ in pairs(global.received_tech) do
tech = force.technologies[item_name]
if tech ~= nil and tech.researched ~= true then
game.print({"", "Received [technology=" .. tech.name .. "] as it is already checked."})
game.play_sound({path="utility/research_completed"})
tech.researched = true
end
end
global.received_tech = {}
else
ALLOW_COLLECT = 0
end
end
end
script.on_event(defines.events.on_runtime_mod_setting_changed, on_runtime_mod_setting_changed)
@@ -658,18 +685,29 @@ commands.add_command("ap-get-technology", "Grant a technology, used by the Archi
if global.index_sync == nil then
global.index_sync = {}
end
if global.received_tech == nil then
global.received_tech = {}
end
local tech
local force = game.forces["player"]
chunks = split(call.parameter, "\t")
local item_name = chunks[1]
local index = chunks[2]
local index = tonumber(chunks[2]) or chunks[2]
local source = chunks[3] or "Archipelago"
if index == -1 then -- for coop sync and restoring from an older savegame
tech = force.technologies[item_name]
if tech == nil then
game.print("Unknown Item " .. item_name)
return
end
if tech.researched ~= true then
game.print({"", "Received [technology=" .. tech.name .. "] as it is already checked."})
game.play_sound({path="utility/research_completed"})
tech.researched = true
if ALLOW_COLLECT == 1 then
game.print({"", "Received [technology=" .. tech.name .. "] as it is already checked."})
game.play_sound({path="utility/research_completed"})
tech.researched = true
else
global.received_tech[item_name] = 1
end
end
return
elseif progressive_technologies[item_name] ~= nil then
@@ -26,6 +26,8 @@ ap-{{ location.address }}-=Researching this technology sends something to someon
[mod-setting-name]
archipelago-death-link-{{ slot_player }}-{{ seed_name }}=Death Link
archipelago-allow-collect-{{ slot_player }}-{{ seed_name }}=Allow Collect
[mod-setting-description]
archipelago-death-link-{{ slot_player }}-{{ seed_name }}=Kill other players in the same Archipelago Multiworld that also have Death Link turned on, when you die.
archipelago-death-link-{{ slot_player }}-{{ seed_name }}=Kill other players in the same Archipelago Multiworld that also have Death Link turned on, when you die.
archipelago-allow-collect-{{ slot_player }}-{{ seed_name }}=Allows released/collected tech locations to be marked as researched automatically.
@@ -26,5 +26,15 @@ data:extend({
{% else %}
default_value = false
{% endif %}
},
{
type = "bool-setting",
name = "archipelago-allow-collect-{{ slot_player }}-{{ seed_name }}",
setting_type = "runtime-global",
{% if allow_collect %}
default_value = true
{% else %}
default_value = false
{% endif %}
}
})
-1
View File
@@ -74,7 +74,6 @@ class FF1World(World):
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"]],
self.player)
terminated_event.access_rule = goal_rule
if "Shard" in items.keys():
def goal_rule_and_shards(state):
return goal_rule(state) and state.has("Shard", self.player, 32)
-1
View File
@@ -187,7 +187,6 @@ item_table = {
"Pazuzu 5F": ItemData(None, ItemClassification.progression),
"Pazuzu 6F": ItemData(None, ItemClassification.progression),
"Dark King": ItemData(None, ItemClassification.progression),
"Tristam Bone Item Given": ItemData(None, ItemClassification.progression),
#"Barred": ItemData(None, ItemClassification.progression),
}
+2 -100
View File
@@ -1,4 +1,4 @@
from Options import Choice, FreeText, Toggle, Range
from Options import Choice, FreeText, Toggle
class Logic(Choice):
@@ -131,21 +131,6 @@ class EnemizerAttacks(Choice):
default = 0
class EnemizerGroups(Choice):
"""Set which enemy groups will be affected by Enemizer."""
display_name = "Enemizer Groups"
option_mobs_only = 0
option_mobs_and_bosses = 1
option_mobs_bosses_and_dark_king = 2
default = 1
class ShuffleResWeakType(Toggle):
"""Resistance and Weakness types are shuffled for all enemies."""
display_name = "Shuffle Resistance/Weakness Types"
default = 0
class ShuffleEnemiesPositions(Toggle):
"""Instead of their original position in a given map, enemies are randomly placed."""
display_name = "Shuffle Enemies' Positions"
@@ -246,81 +231,6 @@ class BattlefieldsBattlesQuantities(Choice):
option_random_one_through_ten = 6
class CompanionLevelingType(Choice):
"""Set how companions gain levels.
Quests: Complete each companion's individual quest for them to promote to their second version.
Quests Extended: Each companion has four exclusive quests, leveling each time a quest is completed.
Save the Crystals (All): Each time a Crystal is saved, all companions gain levels.
Save the Crystals (Individual): Each companion will level to their second version when a specific Crystal is saved.
Benjamin Level: Companions' level tracks Benjamin's."""
option_quests = 0
option_quests_extended = 1
option_save_crystals_individual = 2
option_save_crystals_all = 3
option_benjamin_level = 4
option_benjamin_level_plus_5 = 5
option_benjamin_level_plus_10 = 6
default = 0
display_name = "Companion Leveling Type"
class CompanionSpellbookType(Choice):
"""Update companions' spellbook.
Standard: Original game spellbooks.
Extended: Add some extra spells. Tristam gains Exit and Quake and Reuben gets Blizzard.
Random Balanced: Randomize the spellbooks with an appropriate mix of spells.
Random Chaos: Randomize the spellbooks in total free-for-all."""
option_standard = 0
option_extended = 1
option_random_balanced = 2
option_random_chaos = 3
default = 0
display_name = "Companion Spellbook Type"
class StartingCompanion(Choice):
"""Set a companion to start with.
Random Companion: Randomly select one companion.
Random Plus None: Randomly select a companion, with the possibility of none selected."""
display_name = "Starting Companion"
default = 0
option_none = 0
option_kaeli = 1
option_tristam = 2
option_phoebe = 3
option_reuben = 4
option_random_companion = 5
option_random_plus_none = 6
class AvailableCompanions(Range):
"""Select randomly which companions will join your party. Unavailable companions can still be reached to get their items and complete their quests if needed.
Note: If a Starting Companion is selected, it will always be available, regardless of this setting."""
display_name = "Available Companions"
default = 4
range_start = 0
range_end = 4
class CompanionsLocations(Choice):
"""Set the primary location of companions. Their secondary location is always the same.
Standard: Companions will be at the same locations as in the original game.
Shuffled: Companions' locations are shuffled amongst themselves.
Shuffled Extended: Add all the Temples, as well as Phoebe's House and the Rope Bridge as possible locations."""
display_name = "Companions' Locations"
default = 0
option_standard = 0
option_shuffled = 1
option_shuffled_extended = 2
class KaelisMomFightsMinotaur(Toggle):
"""Transfer Kaeli's requirements (Tree Wither, Elixir) and the two items she's giving to her mom.
Kaeli will be available to join the party right away without the Tree Wither."""
display_name = "Kaeli's Mom Fights Minotaur"
default = 0
option_definitions = {
"logic": Logic,
"brown_boxes": BrownBoxes,
@@ -328,21 +238,12 @@ option_definitions = {
"shattered_sky_coin_quantity": ShatteredSkyCoinQuantity,
"starting_weapon": StartingWeapon,
"progressive_gear": ProgressiveGear,
"leveling_curve": LevelingCurve,
"starting_companion": StartingCompanion,
"available_companions": AvailableCompanions,
"companions_locations": CompanionsLocations,
"kaelis_mom_fight_minotaur": KaelisMomFightsMinotaur,
"companion_leveling_type": CompanionLevelingType,
"companion_spellbook_type": CompanionSpellbookType,
"enemies_density": EnemiesDensity,
"enemies_scaling_lower": EnemiesScalingLower,
"enemies_scaling_upper": EnemiesScalingUpper,
"bosses_scaling_lower": BossesScalingLower,
"bosses_scaling_upper": BossesScalingUpper,
"enemizer_attacks": EnemizerAttacks,
"enemizer_groups": EnemizerGroups,
"shuffle_res_weak_types": ShuffleResWeakType,
"shuffle_enemies_position": ShuffleEnemiesPositions,
"progressive_formations": ProgressiveFormations,
"doom_castle_mode": DoomCastle,
@@ -352,5 +253,6 @@ option_definitions = {
"crest_shuffle": CrestShuffle,
"shuffle_battlefield_rewards": ShuffleBattlefieldRewards,
"map_shuffle_seed": MapShuffleSeed,
"leveling_curve": LevelingCurve,
"battlefields_battles_quantities": BattlefieldsBattlesQuantities,
}
+34 -46
View File
@@ -35,58 +35,46 @@ def generate_output(self, output_directory):
"item_name": location.item.name})
def cc(option):
return option.current_key.title().replace("_", "").replace("OverworldAndDungeons",
"OverworldDungeons").replace("MobsAndBosses", "MobsBosses").replace("MobsBossesAndDarkKing",
"MobsBossesDK").replace("BenjaminLevelPlus", "BenPlus").replace("BenjaminLevel", "BenPlus0").replace(
"RandomCompanion", "Random")
return option.current_key.title().replace("_", "").replace("OverworldAndDungeons", "OverworldDungeons")
def tf(option):
return True if option else False
options = deepcopy(settings_template)
options["name"] = self.multiworld.player_name[self.player]
option_writes = {
"enemies_density": cc(self.multiworld.enemies_density[self.player]),
"chests_shuffle": "Include",
"shuffle_boxes_content": self.multiworld.brown_boxes[self.player] == "shuffle",
"npcs_shuffle": "Include",
"battlefields_shuffle": "Include",
"logic_options": cc(self.multiworld.logic[self.player]),
"shuffle_enemies_position": tf(self.multiworld.shuffle_enemies_position[self.player]),
"enemies_scaling_lower": cc(self.multiworld.enemies_scaling_lower[self.player]),
"enemies_scaling_upper": cc(self.multiworld.enemies_scaling_upper[self.player]),
"bosses_scaling_lower": cc(self.multiworld.bosses_scaling_lower[self.player]),
"bosses_scaling_upper": cc(self.multiworld.bosses_scaling_upper[self.player]),
"enemizer_attacks": cc(self.multiworld.enemizer_attacks[self.player]),
"leveling_curve": cc(self.multiworld.leveling_curve[self.player]),
"battles_quantity": cc(self.multiworld.battlefields_battles_quantities[self.player]) if
self.multiworld.battlefields_battles_quantities[self.player].value < 5 else
"RandomLow" if
self.multiworld.battlefields_battles_quantities[self.player].value == 5 else
"RandomHigh",
"shuffle_battlefield_rewards": tf(self.multiworld.shuffle_battlefield_rewards[self.player]),
"random_starting_weapon": True,
"progressive_gear": tf(self.multiworld.progressive_gear[self.player]),
"tweaked_dungeons": tf(self.multiworld.tweak_frustrating_dungeons[self.player]),
"doom_castle_mode": cc(self.multiworld.doom_castle_mode[self.player]),
"doom_castle_shortcut": tf(self.multiworld.doom_castle_shortcut[self.player]),
"sky_coin_mode": cc(self.multiworld.sky_coin_mode[self.player]),
"sky_coin_fragments_qty": cc(self.multiworld.shattered_sky_coin_quantity[self.player]),
"enable_spoilers": False,
"progressive_formations": cc(self.multiworld.progressive_formations[self.player]),
"map_shuffling": cc(self.multiworld.map_shuffle[self.player]),
"crest_shuffle": tf(self.multiworld.crest_shuffle[self.player]),
"enemizer_groups": cc(self.multiworld.enemizer_groups[self.player]),
"shuffle_res_weak_type": tf(self.multiworld.shuffle_res_weak_types[self.player]),
"companion_leveling_type": cc(self.multiworld.companion_leveling_type[self.player]),
"companion_spellbook_type": cc(self.multiworld.companion_spellbook_type[self.player]),
"starting_companion": cc(self.multiworld.starting_companion[self.player]),
"available_companions": ["Zero", "One", "Two",
"Three", "Four"][self.multiworld.available_companions[self.player].value],
"companions_locations": cc(self.multiworld.companions_locations[self.player]),
"kaelis_mom_fight_minotaur": tf(self.multiworld.kaelis_mom_fight_minotaur[self.player]),
}
option_writes = {
"enemies_density": cc(self.multiworld.enemies_density[self.player]),
"chests_shuffle": "Include",
"shuffle_boxes_content": self.multiworld.brown_boxes[self.player] == "shuffle",
"npcs_shuffle": "Include",
"battlefields_shuffle": "Include",
"logic_options": cc(self.multiworld.logic[self.player]),
"shuffle_enemies_position": tf(self.multiworld.shuffle_enemies_position[self.player]),
"enemies_scaling_lower": cc(self.multiworld.enemies_scaling_lower[self.player]),
"enemies_scaling_upper": cc(self.multiworld.enemies_scaling_upper[self.player]),
"bosses_scaling_lower": cc(self.multiworld.bosses_scaling_lower[self.player]),
"bosses_scaling_upper": cc(self.multiworld.bosses_scaling_upper[self.player]),
"enemizer_attacks": cc(self.multiworld.enemizer_attacks[self.player]),
"leveling_curve": cc(self.multiworld.leveling_curve[self.player]),
"battles_quantity": cc(self.multiworld.battlefields_battles_quantities[self.player]) if
self.multiworld.battlefields_battles_quantities[self.player].value < 5 else
"RandomLow" if
self.multiworld.battlefields_battles_quantities[self.player].value == 5 else
"RandomHigh",
"shuffle_battlefield_rewards": tf(self.multiworld.shuffle_battlefield_rewards[self.player]),
"random_starting_weapon": True,
"progressive_gear": tf(self.multiworld.progressive_gear[self.player]),
"tweaked_dungeons": tf(self.multiworld.tweak_frustrating_dungeons[self.player]),
"doom_castle_mode": cc(self.multiworld.doom_castle_mode[self.player]),
"doom_castle_shortcut": tf(self.multiworld.doom_castle_shortcut[self.player]),
"sky_coin_mode": cc(self.multiworld.sky_coin_mode[self.player]),
"sky_coin_fragments_qty": cc(self.multiworld.shattered_sky_coin_quantity[self.player]),
"enable_spoilers": False,
"progressive_formations": cc(self.multiworld.progressive_formations[self.player]),
"map_shuffling": cc(self.multiworld.map_shuffle[self.player]),
"crest_shuffle": tf(self.multiworld.crest_shuffle[self.player]),
}
for option, data in option_writes.items():
options["Final Fantasy Mystic Quest"][option][data] = 1
@@ -95,7 +83,7 @@ def generate_output(self, output_directory):
'utf8')
self.rom_name_available_event.set()
setup = {"version": "1.5", "name": self.multiworld.player_name[self.player], "romname": rom_name, "seed":
setup = {"version": "1.4", "name": self.multiworld.player_name[self.player], "romname": rom_name, "seed":
hex(self.multiworld.per_slot_randoms[self.player].randint(0, 0xFFFFFFFF)).split("0x")[1].upper()}
starting_items = [output_item_name(item) for item in self.multiworld.precollected_items[self.player]]
+4 -4
View File
@@ -67,10 +67,10 @@ def create_regions(self):
self.multiworld.regions.append(create_region(self.multiworld, self.player, room["name"], room["id"],
[FFMQLocation(self.player, object["name"], location_table[object["name"]] if object["name"] in
location_table else None, object["type"], object["access"],
self.create_item(yaml_item(object["on_trigger"][0])) if object["type"] == "Trigger" else None) for object in
room["game_objects"] if "Hero Chest" not in object["name"] and object["type"] not in ("BattlefieldGp",
"BattlefieldXp") and (object["type"] != "Box" or self.multiworld.brown_boxes[self.player] == "include") and
not (object["name"] == "Kaeli Companion" and not object["on_trigger"])], room["links"]))
self.create_item(yaml_item(object["on_trigger"][0])) if object["type"] == "Trigger" else None) for
object in room["game_objects"] if "Hero Chest" not in object["name"] and object["type"] not in
("BattlefieldGp", "BattlefieldXp") and (object["type"] != "Box" or
self.multiworld.brown_boxes[self.player] == "include")], room["links"]))
dark_king_room = self.multiworld.get_region("Doom Castle Dark King Room", self.player)
dark_king = FFMQLocation(self.player, "Dark King", None, "Trigger", [])
+1 -3
View File
@@ -108,10 +108,8 @@ class FFMQWorld(World):
map_shuffle = multiworld.map_shuffle[world.player].value
crest_shuffle = multiworld.crest_shuffle[world.player].current_key
battlefield_shuffle = multiworld.shuffle_battlefield_rewards[world.player].current_key
companion_shuffle = multiworld.companions_locations[world.player].value
kaeli_mom = multiworld.kaelis_mom_fight_minotaur[world.player].current_key
query = f"s={seed}&m={map_shuffle}&c={crest_shuffle}&b={battlefield_shuffle}&cs={companion_shuffle}&km={kaeli_mom}"
query = f"s={seed}&m={map_shuffle}&c={crest_shuffle}&b={battlefield_shuffle}"
if query in rooms_data:
world.rooms = rooms_data[query]
+5 -30
View File
@@ -827,12 +827,12 @@
id: 164
area: 47
coordinates: [14, 6]
teleporter: [98, 8] # Script for reuben, original value [16, 2]
teleporter: [16, 2]
- name: Fireburg - Hotel
id: 165
area: 47
coordinates: [20, 8]
teleporter: [96, 8] # It's a script now for tristam, original value [17, 2]
teleporter: [17, 2]
- name: Fireburg - GrenadeMan House Script
id: 166
area: 47
@@ -1178,16 +1178,6 @@
area: 60
coordinates: [2, 7]
teleporter: [123, 0]
- name: Lava Dome Pointless Room - Visit Quest Script 1
id: 490
area: 60
coordinates: [4, 4]
teleporter: [99, 8]
- name: Lava Dome Pointless Room - Visit Quest Script 2
id: 491
area: 60
coordinates: [4, 5]
teleporter: [99, 8]
- name: Lava Dome Lower Moon Helm Room - Left Entrance
id: 235
area: 60
@@ -1578,11 +1568,6 @@
area: 79
coordinates: [2, 45]
teleporter: [174, 0]
- name: Mount Gale - Visit Quest
id: 494
area: 79
coordinates: [44, 7]
teleporter: [101, 8]
- name: Windia - Main Entrance 1
id: 312
area: 80
@@ -1628,11 +1613,11 @@
area: 80
coordinates: [21, 39]
teleporter: [30, 5]
- name: Windia - INN's Script # Change to teleporter / Change back to script!
- name: Windia - INN's Script # Change to teleporter
id: 321
area: 80
coordinates: [18, 34]
teleporter: [97, 8] # Original value [79, 8] > [31, 2]
teleporter: [31, 2] # Original value [79, 8]
- name: Windia - Vendor House
id: 322
area: 80
@@ -1712,7 +1697,7 @@
id: 337
area: 82
coordinates: [45, 24]
teleporter: [102, 8] # Changed to script, original value [215, 0]
teleporter: [215, 0]
- name: Windia Inn Lobby - Exit
id: 338
area: 82
@@ -2013,16 +1998,6 @@
area: 95
coordinates: [29, 37]
teleporter: [70, 8]
- name: Light Temple - Visit Quest Script 1
id: 492
area: 95
coordinates: [34, 39]
teleporter: [100, 8]
- name: Light Temple - Visit Quest Script 2
id: 493
area: 95
coordinates: [35, 39]
teleporter: [100, 8]
- name: Ship Dock - Mobius Teleporter Script
id: 397
area: 96
+16 -16
View File
@@ -309,13 +309,13 @@
location: "WindiaBattlefield01"
location_slot: "WindiaBattlefield01"
type: "BattlefieldXp"
access: ["SandCoin", "RiverCoin"]
access: []
- name: "South of Windia Battlefield"
object_id: 0x14
location: "WindiaBattlefield02"
location_slot: "WindiaBattlefield02"
type: "BattlefieldXp"
access: ["SandCoin", "RiverCoin"]
access: []
links:
- target_room: 9 # Focus Tower Windia
location: "FocusTowerWindia"
@@ -739,7 +739,7 @@
object_id: 0x2E
type: "Box"
access: []
- name: "Kaeli Companion"
- name: "Kaeli 1"
object_id: 0
type: "Trigger"
on_trigger: ["Kaeli1"]
@@ -838,7 +838,7 @@
- name: Sand Temple
id: 24
game_objects:
- name: "Tristam Companion"
- name: "Tristam Sand Temple"
object_id: 0
type: "Trigger"
on_trigger: ["Tristam"]
@@ -883,11 +883,6 @@
object_id: 2
type: "NPC"
access: ["Tristam"]
- name: "Tristam Bone Dungeon Item Given"
object_id: 0
type: "Trigger"
on_trigger: ["TristamBoneItemGiven"]
access: ["Tristam"]
links:
- target_room: 25
entrance: 59
@@ -1085,7 +1080,7 @@
object_id: 0x40
type: "Box"
access: []
- name: "Phoebe Companion"
- name: "Phoebe"
object_id: 0
type: "Trigger"
on_trigger: ["Phoebe1"]
@@ -1851,11 +1846,11 @@
access: []
- target_room: 77
entrance: 164
teleporter: [98, 8] # original value [16, 2]
teleporter: [16, 2]
access: []
- target_room: 82
entrance: 165
teleporter: [96, 8] # original value [17, 2]
teleporter: [17, 2]
access: []
- target_room: 208
access: ["Claw"]
@@ -1880,7 +1875,7 @@
object_id: 14
type: "NPC"
access: ["ReubenDadSaved"]
- name: "Reuben Companion"
- name: "Reuben"
object_id: 0
type: "Trigger"
on_trigger: ["Reuben1"]
@@ -1956,7 +1951,12 @@
- name: "Fireburg - Tristam"
object_id: 10
type: "NPC"
access: ["Tristam", "TristamBoneItemGiven"]
access: []
- name: "Tristam Fireburg"
object_id: 0
type: "Trigger"
on_trigger: ["Tristam"]
access: []
links:
- target_room: 76
entrance: 177
@@ -3183,7 +3183,7 @@
access: []
- target_room: 163
entrance: 321
teleporter: [97, 8]
teleporter: [31, 2]
access: []
- target_room: 165
entrance: 322
@@ -3292,7 +3292,7 @@
access: []
- target_room: 164
entrance: 337
teleporter: [102, 8]
teleporter: [215, 0]
access: []
- name: Windia Inn Beds
id: 164
+1 -44
View File
@@ -73,57 +73,14 @@ Final Fantasy Mystic Quest:
Chaos: 0
SelfDestruct: 0
SimpleShuffle: 0
enemizer_groups:
MobsOnly: 0
MobsBosses: 0
MobsBossesDK: 0
shuffle_res_weak_type:
true: 0
false: 0
leveling_curve:
Half: 0
Normal: 0
OneAndHalf: 0
Double: 0
DoubleAndHalf: 0
DoubleHalf: 0
Triple: 0
Quadruple: 0
companion_leveling_type:
Quests: 0
QuestsExtended: 0
SaveCrystalsIndividual: 0
SaveCrystalsAll: 0
BenPlus0: 0
BenPlus5: 0
BenPlus10: 0
companion_spellbook_type:
Standard: 0
Extended: 0
RandomBalanced: 0
RandomChaos: 0
starting_companion:
None: 0
Kaeli: 0
Tristam: 0
Phoebe: 0
Reuben: 0
Random: 0
RandomPlusNone: 0
available_companions:
Zero: 0
One: 0
Two: 0
Three: 0
Four: 0
Random14: 0
Random04: 0
companions_locations:
Standard: 0
Shuffled: 0
ShuffledExtended: 0
kaelis_mom_fight_minotaur:
true: 0
false: 0
battles_quantity:
Ten: 0
Seven: 0
+3 -3
View File
@@ -2,7 +2,7 @@
## Required Software
- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases).
- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases). Make sure to check the box for `SNI Client`
- Hardware or software capable of loading and playing SNES ROM files
- An emulator capable of connecting to SNI such as:
@@ -19,8 +19,8 @@ The Archipelago community cannot supply you with this.
### Windows Setup
1. Download and install [Archipelago](<https://github.com/ArchipelagoMW/Archipelago/releases/latest>). **The installer
file is located in the assets section at the bottom of the version information.**
1. During the installation of Archipelago, you will have been asked to install the SNI Client. If you did not do this,
or you are on an older version, you may run the installer again to install the SNI Client.
2. If you are using an emulator, you should assign your Lua capable emulator as your default program for launching ROM
files.
1. Extract your emulator's folder to your Desktop, or somewhere you will remember.
+4 -8
View File
@@ -1,5 +1,4 @@
import collections
import logging
import typing
from BaseClasses import LocationProgressType, MultiWorld, Location, Region, Entrance
@@ -82,18 +81,15 @@ def locality_rules(world: MultiWorld):
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:
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
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
else:
if not location.event:
location.progress_type = LocationProgressType.EXCLUDED
else:
logging.warning(f"Unable to exclude location {loc_name} in player {player}'s world.")
location.progress_type = LocationProgressType.EXCLUDED
def set_rule(spot: typing.Union["BaseClasses.Location", "BaseClasses.Entrance"], rule: CollectionRule):
+26 -35
View File
@@ -17,22 +17,19 @@ The most recent public release of Archipelago can be found on the GitHub Release
Run the exe file, and after accepting the license agreement you will be asked which components you would like to
install.
Archipelago installations are automatically bundled with some programs. These include a launcher, a generator, a
server and some clients.
The generator allows you to generate multiworld games on your computer. The ROM setups are required if anyone in the
game that you generate wants to play any of those games as they are needed to generate the relevant patch files. If you
do not own the game, uncheck the relevant box. If you gain the game later, the installer can be run again to install and
set up new components.
- The launcher lets you quickly access Archipelago's different components and programs. It is found under the name
`ArchipelagoLauncher` and can be found in the main directory of your Archipelago installation.
- The generator allows you to generate multiworld games on your computer. Please refer to the 'Generating a game'
section of this guide for more information about it.
- The server will allow you to host the multiworld on your machine. Hosting on your machine requires forwarding the port
The server will allow you to host the multiworld on your machine. Hosting on your machine requires forwarding the port
you are hosting on. The default port for Archipelago is `38281`. If you are unsure how to do this there are plenty of
other guides on the internet that will be more suited to your hardware.
- The clients are what are used to connect your game to the multiworld. Some games use a client that is automatically
installed with an Archipelago installation. You can access those clients via the launcher or by navigating
to your Archipelago installation.
The `Clients` are what are used to connect your game to the multiworld. If the game you plan to play is available
here, go ahead and install its client as well. If the game you choose to play is supported by Archipelago but not listed
in the installation, check the setup guide for that game. Installing a client for a ROM based game requires you to have
a legally obtained ROM for that game as well.
## Generating a game
@@ -75,18 +72,14 @@ If you have downloaded the settings, or have created a settings file manually, t
#### On your local installation
To generate a game on your local machine, make sure to install the Archipelago software. Navigate to your Archipelago
To generate a game on your local machine, make sure to install the Archipelago software, and ensure to select the
`Generator` component, as well as the `ROM setup` for any games you will want to play. Navigate to your Archipelago
installation (usually C:\ProgramData\Archipelago), and place the settings file you have either created or downloaded
from the website in the `Players` folder.
Run `ArchipelagoGenerate.exe`, or click on `Generate` in the launcher, and it will inform you whether the generation
was successful or not. If successful, there will be an output zip in the `output` folder
(usually named something like `AP_XXXXX.zip`). This will contain all relevant information to the session, including the
spoiler log, if one was generated.
Please note that some games require you to own their ROM files to generate with them as they are needed to generate the
relevant patch files. When you generate with a ROM game for the first time, you will be asked to locate its base ROM file.
This step only needs to be done once.
Run `ArchipelagoGenerate.exe`, and it will inform you whether the generation was successful or not. If successful, there
will be an output zip in the `output` folder (usually named something like `AP_XXXXX.zip`). This will contain all
relevant information to the session, including the spoiler log, if one was generated.
### Generating a multiplayer game
@@ -104,9 +97,12 @@ player name.
#### On the website
Gather all player YAML files into a single place, then navigate to the [Generate Page](/generate). Select the host settings
you would like, click on `Upload File(s)`, and select all player YAML files. The site also accepts `zip` archives containing YAML
files.
Gather all player YAML files into a single place, and compress them into a zip file. This can be done by pressing
ctrl/cmd + clicking on each file until all are selected, right-clicking one of the files, and clicking
`compress to ZIP file` or `send to > compressed folder`.
Navigate to the [Generate Page](/generate), select the host settings you would like, click on `Upload File`, and
select the newly created zip from the opened window.
After some time, you will be redirected to a seed info page that will display the generated seed, the time it was
created, the number of players, the spoiler (if one was created) and all rooms created from this seed.
@@ -118,11 +114,8 @@ It is possible to generate the multiworld locally, using a local Archipelago ins
Archipelago installation folder (usually C:\ProgramData\Archipelago) and placing each YAML file in the `Players` folder.
If the folder does not exist then it must be created manually. The files here should not be compressed.
After filling the `Players` folder, run`ArchipelagoGenerate.exe` or click `Generate` in the launcher. The output of
the generation is placed in the `output` folder (usually named something like `AP_XXXXX.zip`).
Please note that if any player in the game you want to generate plays a game that needs a ROM file to generate, you will
need the corresponding ROM files.
After filling the `Players` folder, the `ArchipelagoGenerate.exe` program should be run in order to generate a
multiworld. The output of this process is placed in the `output` folder (usually named something like `AP_XXXXX.zip`).
##### Changing local host settings for generation
@@ -130,12 +123,10 @@ Sometimes there are various settings that you may want to change before rolling
auto-release, plando support, or setting a password.
All of these settings, plus other options, may be changed by modifying the `host.yaml` file in the Archipelago
installation folder. You can quickly access this file by clicking on `Open host.yaml` in the launcher. The settings
chosen here are baked into the `.archipelago` file that gets output with the other files after generation, so if you
are rolling locally, ensure this file is edited to your liking **before** rolling the seed. This file is overwritten
when running the Archipelago Installation software. If you have changed settings in this file, and would like to retain
them, you may rename the file to `options.yaml`.
installation folder. The settings chosen here are baked into the `.archipelago` file that gets output with the other
files after generation, so if you are rolling locally, ensure this file is edited to your liking **before** rolling the
seed. This file is overwritten when running the Archipelago Installation software. If you have changed settings in this
file, and would like to retain them, you may rename the file to `options.yaml`.
## Hosting an Archipelago Server
+17 -30
View File
@@ -2,7 +2,7 @@ import typing
from .ExtractedData import logic_options, starts, pool_options
from .Rules import cost_terms
from Options import Option, DefaultOnToggle, Toggle, Choice, Range, OptionDict, NamedRange, DeathLink
from Options import Option, DefaultOnToggle, Toggle, Choice, Range, OptionDict, NamedRange
from .Charms import vanilla_costs, names as charm_names
if typing.TYPE_CHECKING:
@@ -402,34 +402,22 @@ class WhitePalace(Choice):
default = 0
class ExtraPlatforms(DefaultOnToggle):
"""Places additional platforms to make traveling throughout Hallownest more convenient."""
class DeathLinkShade(Choice):
"""Sets whether to create a shade when you are killed by a DeathLink and how to handle your existing shade, if any.
vanilla: DeathLink deaths function like any other death and overrides your existing shade (including geo), if any.
shadeless: DeathLink deaths do not spawn shades. Your existing shade (including geo), if any, is untouched.
shade: DeathLink deaths spawn a shade if you do not have an existing shade. Otherwise, it acts like shadeless.
* This option has no effect if DeathLink is disabled.
** Self-death shade behavior is not changed; if a self-death normally creates a shade in vanilla, it will override
your existing shade, if any.
class DeathLink(Choice):
"""
option_vanilla = 0
When you die, everyone dies. Of course the reverse is true too.
When enabled, choose how incoming deathlinks are handled:
vanilla: DeathLink kills you and is just like any other death. RIP your previous shade and geo.
shadeless: DeathLink kills you, but no shade spawns and no geo is lost. Your previous shade, if any, is untouched.
shade: DeathLink functions like a normal death if you do not already have a shade, shadeless otherwise.
"""
option_off = 0
alias_no = 0
alias_true = 1
alias_on = 1
alias_yes = 1
option_shadeless = 1
option_shade = 2
default = 2
class DeathLinkBreaksFragileCharms(Toggle):
"""Sets if fragile charms break when you are killed by a DeathLink.
* This option has no effect if DeathLink is disabled.
** Self-death fragile charm behavior is not changed; if a self-death normally breaks fragile charms in vanilla, it
will continue to do so.
"""
option_vanilla = 2
option_shade = 3
class StartingGeo(Range):
@@ -488,8 +476,7 @@ hollow_knight_options: typing.Dict[str, type(Option)] = {
**{
option.__name__: option
for option in (
StartLocation, Goal, WhitePalace, ExtraPlatforms, StartingGeo,
DeathLink, DeathLinkShade, DeathLinkBreaksFragileCharms,
StartLocation, Goal, WhitePalace, StartingGeo, DeathLink,
MinimumGeoPrice, MaximumGeoPrice,
MinimumGrubPrice, MaximumGrubPrice,
MinimumEssencePrice, MaximumEssencePrice,
@@ -501,7 +488,7 @@ hollow_knight_options: typing.Dict[str, type(Option)] = {
LegEaterShopSlots, GrubfatherRewardSlots,
SeerRewardSlots, ExtraShopSlots,
SplitCrystalHeart, SplitMothwingCloak, SplitMantisClaw,
CostSanity, CostSanityHybridChance
CostSanity, CostSanityHybridChance,
)
},
**cost_sanity_weights
+11 -10
View File
@@ -419,16 +419,17 @@ class HKWorld(World):
def set_rules(self):
world = self.multiworld
player = self.player
goal = world.Goal[player]
if goal == Goal.option_hollowknight:
world.completion_condition[player] = lambda state: state._hk_can_beat_thk(player)
elif goal == Goal.option_siblings:
world.completion_condition[player] = lambda state: state._hk_siblings_ending(player)
elif goal == Goal.option_radiance:
world.completion_condition[player] = lambda state: state._hk_can_beat_radiance(player)
else:
# Any goal
world.completion_condition[player] = lambda state: state._hk_can_beat_thk(player) or state._hk_can_beat_radiance(player)
if world.logic[player] != 'nologic':
goal = world.Goal[player]
if goal == Goal.option_hollowknight:
world.completion_condition[player] = lambda state: state._hk_can_beat_thk(player)
elif goal == Goal.option_siblings:
world.completion_condition[player] = lambda state: state._hk_siblings_ending(player)
elif goal == Goal.option_radiance:
world.completion_condition[player] = lambda state: state._hk_can_beat_radiance(player)
else:
# Any goal
world.completion_condition[player] = lambda state: state._hk_can_beat_thk(player) or state._hk_can_beat_radiance(player)
set_rules(self)
-2
View File
@@ -444,8 +444,6 @@ def set_rules(hylics2world):
lambda state: paddle(state, player))
add_rule(world.get_location("Arcade 1: Alcove Medallion", 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),
lambda state: bridge_key(state, player))
add_rule(world.get_location("Foglast: Mid-Air Medallion", player),
+20 -80
View File
@@ -29,7 +29,6 @@ class KH2Context(CommonContext):
self.kh2_local_items = None
self.growthlevel = None
self.kh2connected = False
self.kh2_finished_game = False
self.serverconneced = False
self.item_name_to_data = {name: data for name, data, in item_dictionary_table.items()}
self.location_name_to_data = {name: data for name, data, in all_locations.items()}
@@ -80,6 +79,11 @@ class KH2Context(CommonContext):
},
},
}
self.front_of_inventory = {
"Sora": 0x2546,
"Donald": 0x2658,
"Goofy": 0x276C,
}
self.kh2seedname = None
self.kh2slotdata = None
self.itemamount = {}
@@ -164,14 +168,6 @@ class KH2Context(CommonContext):
self.ability_code_list = None
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):
if password_requested and not self.password:
await super(KH2Context, self).server_auth(password_requested)
@@ -222,12 +218,6 @@ class KH2Context(CommonContext):
def kh2_read_byte(self, address):
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):
if cmd in {"RoomInfo"}:
self.kh2seedname = args['seed_name']
@@ -485,7 +475,7 @@ class KH2Context(CommonContext):
async def give_item(self, item, location):
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]
itemdata = self.item_name_to_data[itemname]
# itemcode = self.kh2_item_name_to_id[itemname]
@@ -516,8 +506,6 @@ class KH2Context(CommonContext):
ability_slot = self.kh2_seed_save_cache["GoofyInvo"][1]
self.kh2_seed_save_cache["AmountInvo"]["Ability"][itemname].append(ability_slot)
self.kh2_seed_save_cache["GoofyInvo"][1] -= 2
if ability_slot in self.front_ability_slots:
self.front_ability_slots.remove(ability_slot)
elif len(self.kh2_seed_save_cache["AmountInvo"]["Ability"][itemname]) < \
self.AbilityQuantityDict[itemname]:
@@ -529,14 +517,11 @@ class KH2Context(CommonContext):
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["DonaldInvo"][0] -= 2
else:
elif itemname in self.goofy_ability_set:
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["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}:
# if memaddr is in a bitmask location in memory
if itemname not in self.kh2_seed_save_cache["AmountInvo"]["Bitmask"]:
@@ -629,7 +614,7 @@ class KH2Context(CommonContext):
master_sell = master_equipment | master_staff | master_shield
await asyncio.create_task(self.IsInShop(master_sell))
# print(self.kh2_seed_save_cache["AmountInvo"]["Ability"])
for item_name in master_amount:
item_data = self.item_name_to_data[item_name]
amount_of_items = 0
@@ -687,10 +672,10 @@ class KH2Context(CommonContext):
self.kh2_write_short(self.Save + slot, item_data.memaddr)
# removes the duped ability if client gave faster than the game.
for ability in self.front_ability_slots:
if self.kh2_read_short(self.Save + ability) != 0:
print(f"removed {self.Save + ability} from {ability}")
self.kh2_write_short(self.Save + ability, 0)
for charInvo in {"Sora", "Donald", "Goofy"}:
if self.kh2_read_short(self.Save + self.front_of_inventory[charInvo]) != 0:
print(f"removed {self.Save + self.front_of_inventory[charInvo]} from {charInvo}")
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.
for inventorySlot in {0x25CE, 0x25D0, 0x25D2, 0x25D4, 0x25D6, 0x25D8}:
@@ -754,60 +739,15 @@ class KH2Context(CommonContext):
self.kh2_write_byte(self.Save + item_data.memaddr, amount_of_items)
for item_name in master_stat:
item_data = self.item_name_to_data[item_name]
amount_of_items = 0
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 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)
@@ -893,9 +833,9 @@ async def kh2_watcher(ctx: KH2Context):
await asyncio.create_task(ctx.verifyItems())
await asyncio.create_task(ctx.verifyLevel())
message = [{"cmd": 'LocationChecks', "locations": ctx.sending}]
if finishedGame(ctx, message) and not ctx.kh2_finished_game:
if finishedGame(ctx, message):
await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}])
ctx.kh2_finished_game = True
ctx.finished_game = True
await ctx.send_msgs(message)
elif not ctx.kh2connected and ctx.serverconneced:
logger.info("Game Connection lost. waiting 15 seconds until trying to reconnect.")
+2 -1
View File
@@ -1020,9 +1020,10 @@ def create_regions(self):
multiworld.regions += [create_region(multiworld, player, active_locations, region, locations) for region, locations in
KH2REGIONS.items()]
# fill the event locations with events
multiworld.worlds[player].item_name_to_id.update({event_name: None for event_name in Events_Table})
for location, item in event_location_to_item.items():
multiworld.get_location(location, player).place_locked_item(
multiworld.worlds[player].create_event_item(item))
multiworld.worlds[player].create_item(item))
def connect_regions(self):
+4 -3
View File
@@ -224,7 +224,7 @@ class KH2WorldRules(KH2Rules):
RegionName.Pl2: lambda state: self.pl_unlocked(state, 2),
RegionName.Ag: lambda state: self.ag_unlocked(state, 1),
RegionName.Ag2: lambda state: self.ag_unlocked(state, 2) and self.kh2_has_all([ItemName.FireElement,ItemName.BlizzardElement,ItemName.ThunderElement],state),
RegionName.Ag2: lambda state: self.ag_unlocked(state, 2),
RegionName.Bc: lambda state: self.bc_unlocked(state, 1),
RegionName.Bc2: lambda state: self.bc_unlocked(state, 2),
@@ -268,6 +268,7 @@ class KH2WorldRules(KH2Rules):
add_item_rule(location, lambda item: item.player == self.player and item.name in DonaldAbility_Table.keys())
def set_kh2_goal(self):
final_xemnas_location = self.multiworld.get_location(LocationName.FinalXemnas, self.player)
if self.multiworld.Goal[self.player] == "three_proofs":
final_xemnas_location.access_rule = lambda state: self.kh2_has_all(three_proofs, state)
@@ -290,8 +291,8 @@ class KH2WorldRules(KH2Rules):
else:
self.multiworld.completion_condition[self.player] = lambda state: state.has(ItemName.Bounty, self.player, self.multiworld.BountyRequired[self.player].value)
else:
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)
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)
if self.multiworld.FinalXemnas[self.player]:
self.multiworld.completion_condition[self.player] = lambda state: state.has(ItemName.Victory, self.player, 1)
else:
+2 -6
View File
@@ -119,15 +119,11 @@ class KH2World(World):
item_classification = ItemClassification.useful
else:
item_classification = ItemClassification.filler
created_item = KH2Item(name, item_classification, self.item_name_to_id[name], self.player)
return created_item
def create_event_item(self, name: str) -> Item:
item_classification = ItemClassification.progression
created_item = KH2Item(name, item_classification, None, self.player)
return created_item
def create_items(self) -> None:
"""
Fills ItemPool and manages schmovement, random growth, visit locking and random starting visit locking.
@@ -465,7 +461,7 @@ class KH2World(World):
if location in self.random_super_boss_list:
self.random_super_boss_list.remove(location)
if not self.options.SummonLevelLocationToggle and LocationName.Summonlvl7 in self.random_super_boss_list:
if not self.options.SummonLevelLocationToggle:
self.random_super_boss_list.remove(LocationName.Summonlvl7)
# Testing if the player has the right amount of Bounties for Completion.
+1 -5
View File
@@ -16,7 +16,6 @@ The [player settings page for this game](../player-settings) contains all the op
- Popups
- Get Bonuses
- Form Levels
- Summon Levels
- Sora's Levels
- Keyblade Stats
- Keyblade Abilities
@@ -24,7 +23,7 @@ The [player settings page for this game](../player-settings) contains all the op
<h2 style="text-transform:none";>What Kingdom Hearts 2 items can appear in other players' worlds?</h2>
Every item in the game except for abilities on weapons.
Every item in the game except for party members' abilities.
<h2 style="text-transform:none";>What is The Garden of Assemblage "GoA"?</h2>
@@ -74,8 +73,6 @@ The list of possible locations that can contain a bounty:
- Each of the 13 Data Fights
- Max level (7) for each Drive Form
- Max level (7) of Summons
- Last song of Atlantica
- Sephiroth
- Lingering Will
- Starry Hill
@@ -90,7 +87,6 @@ With the help of Shananas, Num, and ZakTheRobot we have many QoL features such a
- Faster Wardrobe.
- Faster Water Jafar Chase.
- Faster Bulky Vendors
- Carpet Skip.
- Start with Lion Dash.
- Faster Urns.
+7 -7
View File
@@ -7,14 +7,14 @@
<h2 style="text-transform:none";>Required Software:</h2>
`Kingdom Hearts II Final Mix` from the [Epic Games Store](https://store.epicgames.com/en-US/discover/kingdom-hearts)
- Follow this Guide to set up these requirements [KH2Rando.com](https://tommadness.github.io/KH2Randomizer/setup/Panacea-ModLoader/)<br>
1. `3.2.0 OpenKH Mod Manager with Panacea`<br>
2. `Lua Backend from the OpenKH Mod Manager`
3. `Install the mod KH2FM-Mods-Num/GoA-ROM-Edition using OpenKH Mod Manager`<br>
1. `3.0.0 OpenKH Mod Manager with Panacea`<br>
2. `Install mod from KH2FM-Mods-Num/GoA-ROM-Edition`<br>
3. `Setup Lua Backend From the 3.0.0 KH2Randomizer.exe per the setup guide linked above`<br>
- Needed for Archipelago
1. [`ArchipelagoKH2Client.exe`](https://github.com/ArchipelagoMW/Archipelago/releases)<br>
2. `Install the mod from JaredWeakStrike/APCompanion using OpenKH Mod Manager`<br>
3. `Install the mod from KH2FM-Mods-equations19/auto-save using OpenKH Mod Manager`<br>
2. `Install mod from JaredWeakStrike/APCompanion`<br>
3. `Install mod from KH2FM-Mods-equations19/auto-save`<br>
4. `AP Randomizer Seed`
<h3 style="text-transform:none";>Required: Archipelago Companion Mod</h3>
@@ -68,8 +68,8 @@ Enter `The room's port number` into the top box <b> where the x's are</b> and pr
- Run the game in windows/borderless windowed mode. Fullscreen is stable but the game can crash if you alt-tab out.
- Make sure to save in a different save slot when playing in an async or disconnecting from the server to play a different seed
<h2 style="text-transform:none";>Logic Sheet</h2>
Have any questions on what's in logic? This spreadsheet made by Bulcon has the answer [Requirements/logic sheet](https://docs.google.com/spreadsheets/d/1nNi8ohEs1fv-sDQQRaP45o6NoRcMlLJsGckBonweDMY/edit?usp=sharing)
<h2 style="text-transform:none";>Requirement/logic sheet</h2>
Have any questions on what's in logic? This spreadsheet has the answer [Requirements/logic sheet](https://docs.google.com/spreadsheets/d/1Embae0t7pIrbzvX-NRywk7bTHHEvuFzzQBUUpSUL7Ak/edit?usp=sharing)
<h2 style="text-transform:none";>F.A.Q.</h2>
- Why is my HP/MP continuously increasing without stopping?
+7 -8
View File
@@ -349,19 +349,18 @@ class GfxMod(FreeText, LADXROption):
normal = ''
default = 'Link'
__spriteDir: str = Utils.local_path(os.path.join('data', 'sprites','ladx'))
__spriteFiles: typing.DefaultDict[str, typing.List[str]] = defaultdict(list)
__spriteDir: str = None
extensions = [".bin", ".bdiff", ".png", ".bmp"]
for file in os.listdir(__spriteDir):
name, extension = os.path.splitext(file)
if extension in extensions:
__spriteFiles[name].append(file)
def __init__(self, value: str):
super().__init__(value)
if not GfxMod.__spriteDir:
GfxMod.__spriteDir = Utils.local_path(os.path.join('data', 'sprites','ladx'))
for file in os.listdir(GfxMod.__spriteDir):
name, extension = os.path.splitext(file)
if extension in self.extensions:
GfxMod.__spriteFiles[name].append(file)
def verify(self, world, player_name: str, plando_options) -> None:
if self.value == "Link" or self.value in GfxMod.__spriteFiles:
+5 -6
View File
@@ -2,7 +2,7 @@
## Required Software
- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases).
- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases). Make sure to check the box for `Links Awakening DX`
- Software capable of loading and playing GBC ROM files
- [RetroArch](https://retroarch.com?page=platforms) 1.10.3 or newer.
- [BizHawk](https://tasvideos.org/BizHawk) 2.8 or newer.
@@ -10,12 +10,11 @@
## Installation Procedures
1. Download and install [Archipelago](<https://github.com/ArchipelagoMW/Archipelago/releases/latest>). **The installer
file is located in the assets section at the bottom of the version information.**
2. The first time you do local generation or patch your game, you will be asked to locate your base ROM file.
This is your Links Awakening DX ROM file. This only needs to be done once..
1. Download and install LinksAwakeningClient from the link above, making sure to install the most recent version.
**The installer file is located in the assets section at the bottom of the version information**.
- During setup, you will be asked to locate your base ROM file. This is your Links Awakening DX ROM file.
3. You should assign your emulator as your default program for launching ROM
2. You should assign your emulator as your default program for launching ROM
files.
1. Extract your emulator's folder to your Desktop, or somewhere you will remember.
2. Right-click on a ROM file and select **Open with...**
+3 -14
View File
@@ -1,8 +1,6 @@
"""
Archipelago init file for Lingo
"""
from logging import warning
from BaseClasses import Item, ItemClassification, Tutorial
from worlds.AutoWorld import WebWorld, World
from .items import ALL_ITEM_TABLE, LingoItem
@@ -11,6 +9,7 @@ from .options import LingoOptions
from .player_logic import LingoPlayerLogic
from .regions import create_regions
from .static_logic import Room, RoomEntrance
from .testing import LingoTestOptions
class LingoWebWorld(WebWorld):
@@ -50,14 +49,6 @@ class LingoWorld(World):
player_logic: LingoPlayerLogic
def generate_early(self):
if not (self.options.shuffle_doors or self.options.shuffle_colors):
if self.multiworld.players == 1:
warning(f"{self.multiworld.get_player_name(self.player)}'s Lingo world doesn't have any progression"
f" items. Please turn on Door Shuffle or Color Shuffle if that doesn't seem right.")
else:
raise Exception(f"{self.multiworld.get_player_name(self.player)}'s Lingo world doesn't have any"
f" progression items. Please turn on Door Shuffle or Color Shuffle.")
self.player_logic = LingoPlayerLogic(self)
def create_regions(self):
@@ -103,11 +94,9 @@ class LingoWorld(World):
classification = item.classification
if hasattr(self, "options") and self.options.shuffle_paintings and len(item.painting_ids) > 0\
and len(item.door_ids) == 0 and all(painting_id not in self.player_logic.painting_mapping
for painting_id in item.painting_ids)\
and "pilgrim_painting2" not in item.painting_ids:
for painting_id in item.painting_ids):
# If this is a "door" that just moves one or more paintings, and painting shuffle is on and those paintings
# go nowhere, then this item should not be progression. The Pilgrim Room painting is special and needs to be
# excluded from this.
# go nowhere, then this item should not be progression.
classification = ItemClassification.filler
return LingoItem(name, classification, item.code, self.player)
+24 -88
View File
@@ -373,7 +373,6 @@
ANOTHER TRY:
id: Entry Room/Panel_advance
tag: topwhite
non_counting: True # This is a counting panel in-game, but it can never count towards the LEVEL 2 panel hunt.
LEVEL 2:
# We will set up special rules for this in code.
id: EndPanel/Panel_level_2
@@ -1034,8 +1033,6 @@
Hallway Room (3): True
Hallway Room (4): True
Hedge Maze: True # through the door to the sectioned-off part of the hedge maze
Cellar:
door: Lookout Entrance
panels:
MASSACRED:
id: Palindrome Room/Panel_massacred_sacred
@@ -1171,21 +1168,11 @@
- KEEP
- BAILEY
- TOWER
Lookout Entrance:
id: Cross Room Doors/Door_missing
location_name: Outside The Agreeable - Lookout Panels
panels:
- NORTH
- WINTER
- DIAMONDS
- FIRE
paintings:
- id: panda_painting
orientation: south
- id: eyes_yellow_painting
orientation: east
- id: pencil_painting7
orientation: north
progression:
Progressive Hallway Room:
- Hallway Door
@@ -2056,7 +2043,7 @@
door: Sixth Floor
Cellar:
room: Room Room
door: Cellar Exit
door: Shortcut to Fifth Floor
Welcome Back Area:
door: Welcome Back
Art Gallery:
@@ -2315,6 +2302,9 @@
id: Master Room/Panel_mastery_mastery3
tag: midwhite
hunt: True
required_door:
room: Orange Tower Seventh Floor
door: Mastery
THE LIBRARY:
id: EndPanel/Panel_library
check: True
@@ -2685,10 +2675,6 @@
Outside The Undeterred: True
Outside The Agreeable: True
Outside The Wanderer: True
The Observant: True
Art Gallery: True
The Scientific: True
Cellar: True
Orange Tower Fifth Floor:
room: Orange Tower Fifth Floor
door: Welcome Back
@@ -3005,7 +2991,8 @@
PATS:
id: Rhyme Room/Panel_wrath_path
colors: purple
tag: forbid
tag: midpurp and rhyme
copy_to_sign: sign15
KNIGHT:
id: Rhyme Room/Panel_knight_write
colors: purple
@@ -3171,8 +3158,6 @@
door: Painting Shortcut
painting: True
Room Room: True # trapdoor
Outside The Agreeable:
painting: True
panels:
UNOPEN:
id: Truncate Room/Panel_unopened_open
@@ -6314,22 +6299,17 @@
SKELETON:
id: Double Room/Panel_bones_syn
tag: syn rhyme
colors: purple
subtag: bot
link: rhyme BONES
REPENTANCE:
id: Double Room/Panel_sentence_rhyme
colors:
- purple
- blue
colors: purple
tag: whole rhyme
subtag: top
link: rhyme SENTENCE
WORD:
id: Double Room/Panel_sentence_whole
colors:
- purple
- blue
colors: blue
tag: whole rhyme
subtag: bot
link: rhyme SENTENCE
@@ -6341,7 +6321,6 @@
link: rhyme DREAM
FANTASY:
id: Double Room/Panel_dream_syn
colors: purple
tag: syn rhyme
subtag: bot
link: rhyme DREAM
@@ -6353,7 +6332,6 @@
link: rhyme MYSTERY
SECRET:
id: Double Room/Panel_mystery_syn
colors: purple
tag: syn rhyme
subtag: bot
link: rhyme MYSTERY
@@ -6408,33 +6386,25 @@
door: Nines
FERN:
id: Double Room/Panel_return_rhyme
colors:
- purple
- black
colors: purple
tag: ant rhyme
subtag: top
link: rhyme RETURN
STAY:
id: Double Room/Panel_return_ant
colors:
- purple
- black
colors: black
tag: ant rhyme
subtag: bot
link: rhyme RETURN
FRIEND:
id: Double Room/Panel_descend_rhyme
colors:
- purple
- black
colors: purple
tag: ant rhyme
subtag: top
link: rhyme DESCEND
RISE:
id: Double Room/Panel_descend_ant
colors:
- purple
- black
colors: black
tag: ant rhyme
subtag: bot
link: rhyme DESCEND
@@ -6446,7 +6416,6 @@
link: rhyme JUMP
BOUNCE:
id: Double Room/Panel_jump_syn
colors: purple
tag: syn rhyme
subtag: bot
link: rhyme JUMP
@@ -6458,7 +6427,6 @@
link: rhyme FALL
PLUNGE:
id: Double Room/Panel_fall_syn
colors: purple
tag: syn rhyme
subtag: bot
link: rhyme FALL
@@ -6488,17 +6456,13 @@
panels:
BIRD:
id: Double Room/Panel_word_rhyme
colors:
- purple
- blue
colors: purple
tag: whole rhyme
subtag: top
link: rhyme WORD
LETTER:
id: Double Room/Panel_word_whole
colors:
- purple
- blue
colors: blue
tag: whole rhyme
subtag: bot
link: rhyme WORD
@@ -6510,7 +6474,6 @@
link: rhyme HIDDEN
CONCEALED:
id: Double Room/Panel_hidden_syn
colors: purple
tag: syn rhyme
subtag: bot
link: rhyme HIDDEN
@@ -6522,7 +6485,6 @@
link: rhyme SILENT
MUTE:
id: Double Room/Panel_silent_syn
colors: purple
tag: syn rhyme
subtag: bot
link: rhyme SILENT
@@ -6569,7 +6531,6 @@
link: rhyme BLOCKED
OBSTRUCTED:
id: Double Room/Panel_blocked_syn
colors: purple
tag: syn rhyme
subtag: bot
link: rhyme BLOCKED
@@ -6581,7 +6542,6 @@
link: rhyme RISE
SWELL:
id: Double Room/Panel_rise_syn
colors: purple
tag: syn rhyme
subtag: bot
link: rhyme RISE
@@ -6593,7 +6553,6 @@
link: rhyme ASCEND
CLIMB:
id: Double Room/Panel_ascend_syn
colors: purple
tag: syn rhyme
subtag: bot
link: rhyme ASCEND
@@ -6605,7 +6564,6 @@
link: rhyme DOUBLE
DUPLICATE:
id: Double Room/Panel_double_syn
colors: purple
tag: syn rhyme
subtag: bot
link: rhyme DOUBLE
@@ -6684,7 +6642,6 @@
link: rhyme CHILD
KID:
id: Double Room/Panel_child_syn
colors: purple
tag: syn rhyme
subtag: bot
link: rhyme CHILD
@@ -6696,7 +6653,6 @@
link: rhyme CRYSTAL
QUARTZ:
id: Double Room/Panel_crystal_syn
colors: purple
tag: syn rhyme
subtag: bot
link: rhyme CRYSTAL
@@ -6708,7 +6664,6 @@
link: rhyme CREATIVE
INNOVATIVE (Bottom):
id: Double Room/Panel_creative_syn
colors: purple
tag: syn rhyme
subtag: bot
link: rhyme CREATIVE
@@ -6927,7 +6882,7 @@
event: True
panels:
- WALL (1)
Cellar Exit:
Shortcut to Fifth Floor:
id:
- Tower Room Area Doors/Door_panel_basement
- Tower Room Area Doors/Door_panel_basement2
@@ -6940,10 +6895,7 @@
door: Excavation
Orange Tower Fifth Floor:
room: Room Room
door: Cellar Exit
Outside The Agreeable:
room: Outside The Agreeable
door: Lookout Entrance
door: Shortcut to Fifth Floor
Outside The Wise:
entrances:
Orange Tower Sixth Floor:
@@ -7367,65 +7319,49 @@
link: change GRAVITY
PART:
id: Chemistry Room/Panel_physics_2
colors:
- blue
- red
colors: blue
tag: blue mid red bot
subtag: mid
link: xur PARTICLE
MATTER:
id: Chemistry Room/Panel_physics_1
colors:
- blue
- red
colors: red
tag: blue mid red bot
subtag: bot
link: xur PARTICLE
ELECTRIC:
id: Chemistry Room/Panel_physics_6
colors:
- purple
- red
colors: purple
tag: purple mid red bot
subtag: mid
link: xpr ELECTRON
ATOM (1):
id: Chemistry Room/Panel_physics_3
colors:
- purple
- red
colors: red
tag: purple mid red bot
subtag: bot
link: xpr ELECTRON
NEUTRAL:
id: Chemistry Room/Panel_physics_7
colors:
- purple
- red
colors: purple
tag: purple mid red bot
subtag: mid
link: xpr NEUTRON
ATOM (2):
id: Chemistry Room/Panel_physics_4
colors:
- purple
- red
colors: red
tag: purple mid red bot
subtag: bot
link: xpr NEUTRON
PROPEL:
id: Chemistry Room/Panel_physics_8
colors:
- purple
- red
colors: purple
tag: purple mid red bot
subtag: mid
link: xpr PROTON
ATOM (3):
id: Chemistry Room/Panel_physics_5
colors:
- purple
- red
colors: red
tag: purple mid red bot
subtag: bot
link: xpr PROTON
+1 -4
View File
@@ -1064,9 +1064,6 @@ doors:
Hallway Door:
item: 444459
location: 445214
Lookout Entrance:
item: 444579
location: 445271
Dread Hallway:
Tenacious Entrance:
item: 444462
@@ -1405,7 +1402,7 @@ doors:
item: 444570
location: 445266
Room Room:
Cellar Exit:
Shortcut to Fifth Floor:
item: 444571
location: 445076
Outside The Wise:
+1 -1
View File
@@ -32,7 +32,7 @@ class LocationChecks(Choice):
option_insanity = 2
class ShuffleColors(DefaultOnToggle):
class ShuffleColors(Toggle):
"""If on, an item is added to the pool for every puzzle color (besides White).
You will need to unlock the requisite colors in order to be able to solve puzzles of that color."""
display_name = "Shuffle Colors"
+9 -25
View File
@@ -6,6 +6,7 @@ from .options import LocationChecks, ShuffleDoors, VictoryCondition
from .static_logic import DOORS_BY_ROOM, Door, PAINTINGS, PAINTINGS_BY_ROOM, PAINTING_ENTRANCES, PAINTING_EXITS, \
PANELS_BY_ROOM, PROGRESSION_BY_ROOM, REQUIRED_PAINTING_ROOMS, REQUIRED_PAINTING_WHEN_NO_DOORS_ROOMS, RoomAndDoor, \
RoomAndPanel
from .testing import LingoTestOptions
if TYPE_CHECKING:
from . import LingoWorld
@@ -189,25 +190,6 @@ class LingoPlayerLogic:
if item.should_include(world):
self.real_items.append(name)
# Calculate the requirements for the fake pilgrimage.
fake_pilgrimage = [
["Second Room", "Exit Door"], ["Crossroads", "Tower Entrance"],
["Orange Tower Fourth Floor", "Hot Crusts Door"], ["Outside The Initiated", "Shortcut to Hub Room"],
["Orange Tower First Floor", "Shortcut to Hub Room"], ["Directional Gallery", "Shortcut to The Undeterred"],
["Orange Tower First Floor", "Salt Pepper Door"], ["Hub Room", "Crossroads Entrance"],
["Champion's Rest", "Shortcut to The Steady"], ["The Bearer", "Shortcut to The Bold"],
["Art Gallery", "Exit"], ["The Tenacious", "Shortcut to Hub Room"],
["Outside The Agreeable", "Tenacious Entrance"]
]
pilgrimage_reqs = AccessRequirements()
for door in fake_pilgrimage:
door_object = DOORS_BY_ROOM[door[0]][door[1]]
if door_object.event or world.options.shuffle_doors == ShuffleDoors.option_none:
pilgrimage_reqs.merge(self.calculate_door_requirements(door[0], door[1], world))
else:
pilgrimage_reqs.doors.add(RoomAndDoor(door[0], door[1]))
self.door_reqs.setdefault("Pilgrim Antechamber", {})["Pilgrimage"] = pilgrimage_reqs
# Create the paintings mapping, if painting shuffle is on.
if painting_shuffle:
# Shuffle paintings until we get something workable.
@@ -223,7 +205,7 @@ class LingoPlayerLogic:
"kind of logic error.")
if door_shuffle != ShuffleDoors.option_none and location_classification != LocationClassification.insanity \
and not early_color_hallways is False:
and not early_color_hallways and LingoTestOptions.disable_forced_good_item is False:
# If shuffle doors is on, force a useful item onto the HI panel. This may not necessarily get you out of BK,
# but the goal is to allow you to reach at least one more check. The non-painting ones are hardcoded right
# now. We only allow the entrance to the Pilgrim Room if color shuffle is off, because otherwise there are
@@ -387,9 +369,11 @@ class LingoPlayerLogic:
door_object = DOORS_BY_ROOM[room][door]
for req_panel in door_object.panels:
panel_room = room if req_panel.room is None else req_panel.room
access_reqs.rooms.add(panel_room)
sub_access_reqs = self.calculate_panel_requirements(panel_room, req_panel.panel, world)
if req_panel.room is not None and req_panel.room != room:
access_reqs.rooms.add(req_panel.room)
sub_access_reqs = self.calculate_panel_requirements(room if req_panel.room is None else req_panel.room,
req_panel.panel, world)
access_reqs.merge(sub_access_reqs)
self.door_reqs[room][door] = access_reqs
@@ -413,8 +397,8 @@ class LingoPlayerLogic:
unhindered_panels_by_color: dict[Optional[str], int] = {}
for panel_name, panel_data in room_data.items():
# We won't count non-counting panels. THE MASTER has special access rules and is handled separately.
if panel_data.non_counting or panel_name == "THE MASTER":
# We won't count non-counting panels.
if panel_data.non_counting:
continue
# We won't coalesce any panels that have requirements beyond colors. To simplify things for now, we will
+11 -4
View File
@@ -4,7 +4,7 @@ from BaseClasses import Entrance, ItemClassification, Region
from .items import LingoItem
from .locations import LingoLocation
from .player_logic import LingoPlayerLogic
from .rules import lingo_can_use_entrance, make_location_lambda
from .rules import lingo_can_use_entrance, lingo_can_use_pilgrimage, make_location_lambda
from .static_logic import ALL_ROOMS, PAINTINGS, Room, RoomAndDoor
if TYPE_CHECKING:
@@ -25,6 +25,15 @@ def create_region(room: Room, world: "LingoWorld", player_logic: LingoPlayerLogi
return new_region
def handle_pilgrim_room(regions: Dict[str, Region], world: "LingoWorld", player_logic: LingoPlayerLogic) -> None:
target_region = regions["Pilgrim Antechamber"]
source_region = regions["Outside The Agreeable"]
source_region.connect(
target_region,
"Pilgrimage",
lambda state: lingo_can_use_pilgrimage(state, world, player_logic))
def connect_entrance(regions: Dict[str, Region], source_region: Region, target_region: Region, description: str,
door: Optional[RoomAndDoor], world: "LingoWorld", player_logic: LingoPlayerLogic):
connection = Entrance(world.player, description, source_region)
@@ -82,9 +91,7 @@ def create_regions(world: "LingoWorld", player_logic: LingoPlayerLogic) -> None:
connect_entrance(regions, regions[entrance.room], regions[room.name], entrance_name, entrance.door, world,
player_logic)
# Add the fake pilgrimage.
connect_entrance(regions, regions["Outside The Agreeable"], regions["Pilgrim Antechamber"], "Pilgrimage",
RoomAndDoor("Pilgrim Antechamber", "Pilgrimage"), world, player_logic)
handle_pilgrim_room(regions, world, player_logic)
if early_color_hallways:
regions["Starting Room"].connect(regions["Outside The Undeterred"], "Early Color Hallways")
+17 -6
View File
@@ -17,6 +17,23 @@ def lingo_can_use_entrance(state: CollectionState, room: str, door: RoomAndDoor,
return _lingo_can_open_door(state, effective_room, door.door, world, player_logic)
def lingo_can_use_pilgrimage(state: CollectionState, world: "LingoWorld", player_logic: LingoPlayerLogic):
fake_pilgrimage = [
["Second Room", "Exit Door"], ["Crossroads", "Tower Entrance"],
["Orange Tower Fourth Floor", "Hot Crusts Door"], ["Outside The Initiated", "Shortcut to Hub Room"],
["Orange Tower First Floor", "Shortcut to Hub Room"], ["Directional Gallery", "Shortcut to The Undeterred"],
["Orange Tower First Floor", "Salt Pepper Door"], ["Hub Room", "Crossroads Entrance"],
["Champion's Rest", "Shortcut to The Steady"], ["The Bearer", "Shortcut to The Bold"],
["Art Gallery", "Exit"], ["The Tenacious", "Shortcut to Hub Room"],
["Outside The Agreeable", "Tenacious Entrance"]
]
for entrance in fake_pilgrimage:
if not _lingo_can_open_door(state, entrance[0], entrance[1], world, player_logic):
return False
return True
def lingo_can_use_location(state: CollectionState, location: PlayerLocation, world: "LingoWorld",
player_logic: LingoPlayerLogic):
return _lingo_can_satisfy_requirements(state, location.access, world, player_logic)
@@ -39,12 +56,6 @@ def lingo_can_use_level_2_location(state: CollectionState, world: "LingoWorld",
counted_panels += panel_count
if counted_panels >= world.options.level_2_requirement.value - 1:
return True
# THE MASTER has to be handled separately, because it has special access rules.
if state.can_reach("Orange Tower Seventh Floor", "Region", world.player)\
and lingo_can_use_mastery_location(state, world, player_logic):
counted_panels += 1
if counted_panels >= world.options.level_2_requirement.value - 1:
return True
return False
+3 -16
View File
@@ -3,13 +3,10 @@ from . import LingoTestBase
class TestRequiredRoomLogic(LingoTestBase):
options = {
"shuffle_doors": "complex",
"shuffle_colors": "false",
"shuffle_doors": "complex"
}
def test_pilgrim_first(self) -> None:
self.remove_forced_good_item()
self.assertFalse(self.multiworld.state.can_reach("The Seeker", "Region", self.player))
self.assertFalse(self.multiworld.state.can_reach("Pilgrim Antechamber", "Region", self.player))
self.assertFalse(self.multiworld.state.can_reach("Pilgrim Room", "Region", self.player))
@@ -30,8 +27,6 @@ class TestRequiredRoomLogic(LingoTestBase):
self.assertTrue(self.can_reach_location("The Seeker - Achievement"))
def test_hidden_first(self) -> None:
self.remove_forced_good_item()
self.assertFalse(self.multiworld.state.can_reach("The Seeker", "Region", self.player))
self.assertFalse(self.multiworld.state.can_reach("Pilgrim Room", "Region", self.player))
self.assertFalse(self.can_reach_location("The Seeker - Achievement"))
@@ -54,13 +49,10 @@ class TestRequiredRoomLogic(LingoTestBase):
class TestRequiredDoorLogic(LingoTestBase):
options = {
"shuffle_doors": "complex",
"shuffle_colors": "false",
"shuffle_doors": "complex"
}
def test_through_rhyme(self) -> None:
self.remove_forced_good_item()
self.assertFalse(self.can_reach_location("Rhyme Room - Circle/Looped Square Wall"))
self.collect_by_name("Starting Room - Rhyme Room Entrance")
@@ -70,8 +62,6 @@ class TestRequiredDoorLogic(LingoTestBase):
self.assertTrue(self.can_reach_location("Rhyme Room - Circle/Looped Square Wall"))
def test_through_hidden(self) -> None:
self.remove_forced_good_item()
self.assertFalse(self.can_reach_location("Rhyme Room - Circle/Looped Square Wall"))
self.collect_by_name("Starting Room - Rhyme Room Entrance")
@@ -86,13 +76,10 @@ class TestRequiredDoorLogic(LingoTestBase):
class TestSimpleDoors(LingoTestBase):
options = {
"shuffle_doors": "simple",
"shuffle_colors": "false",
"shuffle_doors": "simple"
}
def test_requirement(self):
self.remove_forced_good_item()
self.assertFalse(self.multiworld.state.can_reach("Outside The Wanderer", "Region", self.player))
self.assertFalse(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player))
-4
View File
@@ -8,8 +8,6 @@ class TestProgressiveOrangeTower(LingoTestBase):
}
def test_from_welcome_back(self) -> None:
self.remove_forced_good_item()
self.assertFalse(self.multiworld.state.can_reach("Orange Tower First Floor", "Region", self.player))
self.assertFalse(self.multiworld.state.can_reach("Orange Tower Second Floor", "Region", self.player))
self.assertFalse(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player))
@@ -85,8 +83,6 @@ class TestProgressiveOrangeTower(LingoTestBase):
self.assertTrue(self.multiworld.state.can_reach("Orange Tower Seventh Floor", "Region", self.player))
def test_from_hub_room(self) -> None:
self.remove_forced_good_item()
self.assertFalse(self.multiworld.state.can_reach("Orange Tower First Floor", "Region", self.player))
self.assertFalse(self.multiworld.state.can_reach("Orange Tower Second Floor", "Region", self.player))
self.assertFalse(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player))
+1 -8
View File
@@ -7,8 +7,6 @@ class TestComplexProgressiveHallwayRoom(LingoTestBase):
}
def test_item(self):
self.remove_forced_good_item()
self.assertFalse(self.multiworld.state.can_reach("Outside The Agreeable", "Region", self.player))
self.assertFalse(self.multiworld.state.can_reach("Hallway Room (2)", "Region", self.player))
self.assertFalse(self.multiworld.state.can_reach("Hallway Room (3)", "Region", self.player))
@@ -60,8 +58,6 @@ class TestSimpleHallwayRoom(LingoTestBase):
}
def test_item(self):
self.remove_forced_good_item()
self.assertFalse(self.multiworld.state.can_reach("Outside The Agreeable", "Region", self.player))
self.assertFalse(self.multiworld.state.can_reach("Hallway Room (2)", "Region", self.player))
self.assertFalse(self.multiworld.state.can_reach("Hallway Room (3)", "Region", self.player))
@@ -85,13 +81,10 @@ class TestSimpleHallwayRoom(LingoTestBase):
class TestProgressiveArtGallery(LingoTestBase):
options = {
"shuffle_doors": "complex",
"shuffle_colors": "false",
"shuffle_doors": "complex"
}
def test_item(self):
self.remove_forced_good_item()
self.assertFalse(self.multiworld.state.can_reach("Art Gallery", "Region", self.player))
self.assertFalse(self.multiworld.state.can_reach("Art Gallery (Second Floor)", "Region", self.player))
self.assertFalse(self.multiworld.state.can_reach("Art Gallery (Third Floor)", "Region", self.player))
+2 -6
View File
@@ -1,6 +1,7 @@
from typing import ClassVar
from test.bases import WorldTestBase
from .. import LingoTestOptions
class LingoTestBase(WorldTestBase):
@@ -8,10 +9,5 @@ class LingoTestBase(WorldTestBase):
player: ClassVar[int] = 1
def world_setup(self, *args, **kwargs):
LingoTestOptions.disable_forced_good_item = True
super().world_setup(*args, **kwargs)
def remove_forced_good_item(self):
location = self.multiworld.get_location("Second Room - Good Luck", self.player)
self.remove(location.item)
self.multiworld.itempool.append(location.item)
self.multiworld.state.events.add(location)
+2
View File
@@ -0,0 +1,2 @@
class LingoTestOptions:
disable_forced_good_item: bool = False
+1 -1
View File
@@ -40,7 +40,7 @@ mentioned_panels = Set[]
door_groups = {}
directives = Set["entrances", "panels", "doors", "paintings", "progression"]
panel_directives = Set["id", "required_room", "required_door", "required_panel", "colors", "check", "exclude_reduce", "tag", "link", "subtag", "achievement", "copy_to_sign", "non_counting", "hunt"]
panel_directives = Set["id", "required_room", "required_door", "required_panel", "colors", "check", "exclude_reduce", "tag", "link", "subtag", "achievement", "copy_to_sign", "non_counting"]
door_directives = Set["id", "painting_id", "panels", "item_name", "location_name", "skip_location", "skip_item", "group", "include_reduce", "junk_item", "event"]
painting_directives = Set["id", "enter_only", "exit_only", "orientation", "required_door", "required", "required_when_no_doors", "move", "req_blocked", "req_blocked_when_no_doors"]
-6
View File
@@ -170,9 +170,6 @@ pullpc
ScriptTX:
STA $7FD4F1 ; (overwritten instruction)
LDA $05AC ; load map number
CMP.b #$F1 ; check if ancient cave final floor
BNE +
REP #$20
LDA $7FD4EF ; read script item id
CMP.w #$01C2 ; test for ancient key
@@ -264,9 +261,6 @@ SpecialItemGet:
BRA ++
+: CMP.w #$01C2 ; ancient key
BNE +
LDA.w #$0008
ORA $0796
STA $0796 ; set ancient key EV flag ($C3)
LDA.w #$0200
ORA $0797
STA $0797 ; set boss item EV flag ($D1)
Binary file not shown.
+5 -6
View File
@@ -2,7 +2,7 @@
## Required Software
- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases).
- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases). Make sure to check the box for `SNI Client - Lufia II Ancient Cave Patch Setup`
- Hardware or software capable of loading and playing SNES ROM files
- An emulator capable of connecting to SNI
([snes9x rr](https://github.com/gocha/snes9x-rr/releases),
@@ -14,12 +14,11 @@ modded SNES minis are currently not supported by SNI**
## Installation Procedures
1. Download and install [Archipelago](<https://github.com/ArchipelagoMW/Archipelago/releases/latest>). **The installer
file is located in the assets section at the bottom of the version information.**
2. The first time you do local generation or patch your game, you will be asked to locate your base ROM file.
This is your American Lufia II - Rise of the Sinistrals ROM file. This only needs to be done once.
1. Download and install SNIClient from the link above, making sure to install the most recent version.
**The installer file is located in the assets section at the bottom of the version information**.
- During setup, you will be asked to locate your base ROM file. This is your American Lufia II - Rise of the Sinistrals ROM file.
3. If you are using an emulator, you should assign your Lua capable emulator as your default program for launching ROM
2. If you are using an emulator, you should assign your Lua capable emulator as your default program for launching ROM
files.
1. Extract your emulator's folder to your Desktop, or somewhere you will remember.
2. Right-click on a ROM file and select **Open with...**
+1 -1
View File
@@ -62,7 +62,7 @@ class MessengerWorld(World):
"Money Wrench",
], base_offset)}
required_client_version = (0, 4, 2)
required_client_version = (0, 4, 1)
web = MessengerWeb()
+4 -2
View File
@@ -1,10 +1,12 @@
# The Messenger
## Quick Links
- [Setup](/tutorial/The%20Messenger/setup/en)
- [Options Page](/games/The%20Messenger/player-options)
- [Setup](../../../../tutorial/The%20Messenger/setup/en)
- [Settings Page](../../../../games/The%20Messenger/player-settings)
- [Courier Github](https://github.com/Brokemia/Courier)
- [The Messenger Randomizer Github](https://github.com/minous27/TheMessengerRandomizerMod)
- [The Messenger Randomizer AP Github](https://github.com/alwaysintreble/TheMessengerRandomizerModAP)
- [Jacksonbird8237's Item Tracker](https://github.com/Jacksonbird8237/TheMessengerItemTracker)
- [PopTracker Pack](https://github.com/alwaysintreble/TheMessengerTrackPack)
## What does randomization do in this game?
+4 -3
View File
@@ -1,15 +1,16 @@
# The Messenger Randomizer Setup Guide
## Quick Links
- [Game Info](/games/The%20Messenger/info/en)
- [Options Page](/games/The%20Messenger/player-options)
- [Game Info](../../../../games/The%20Messenger/info/en)
- [Settings Page](../../../../games/The%20Messenger/player-settings)
- [Courier Github](https://github.com/Brokemia/Courier)
- [The Messenger Randomizer AP Github](https://github.com/alwaysintreble/TheMessengerRandomizerModAP)
- [Jacksonbird8237's Item Tracker](https://github.com/Jacksonbird8237/TheMessengerItemTracker)
- [PopTracker Pack](https://github.com/alwaysintreble/TheMessengerTrackPack)
## Installation
1. Read the [Game Info Page](/games/The%20Messenger/info/en) for how the game works, caveats and known issues
1. Read the [Game Info Page](../../../../games/The%20Messenger/info/en) for how the game works, caveats and known issues
2. Download and install Courier Mod Loader using the instructions on the release page
* [Latest release is currently 0.7.1](https://github.com/Brokemia/Courier/releases)
3. Download and install the randomizer mod
+1 -4
View File
@@ -63,10 +63,7 @@ class MessengerRules:
"Searing Crags Seal - Triple Ball Spinner": self.has_vertical,
"Searing Crags - Astral Tea Leaves":
lambda state: state.can_reach("Ninja Village - Astral Seed", "Location", self.player),
"Searing Crags - Key of Strength": lambda state: state.has("Power Thistle", self.player)
and (self.has_dart(state)
or (self.has_wingsuit(state)
and self.can_destroy_projectiles(state))),
"Searing Crags - Key of Strength": lambda state: state.has("Power Thistle", self.player),
# glacial peak
"Glacial Peak Seal - Ice Climbers": self.has_dart,
"Glacial Peak Seal - Projectile Spike Pit": self.can_destroy_projectiles,
+1
View File
@@ -5,6 +5,7 @@
- Minecraft Java Edition from
the [Minecraft Java Edition Store Page](https://www.minecraft.net/en-us/store/minecraft-java-edition)
- Archipelago from the [Archipelago Releases Page](https://github.com/ArchipelagoMW/Archipelago/releases)
- (select `Minecraft Client` during installation.)
## Configuring your YAML file
+2 -1
View File
@@ -10,7 +10,8 @@ As we are using Bizhawk, this guide is only applicable to Windows and Linux syst
- Version 2.7.0 and later are supported.
- Detailed installation instructions for Bizhawk can be found at the above link.
- Windows users must run the prereq installer first, which can also be found at the above link.
- The built-in Archipelago client, which can be installed [here](https://github.com/ArchipelagoMW/Archipelago/releases).
- The built-in Archipelago client, which can be installed [here](https://github.com/ArchipelagoMW/Archipelago/releases)
(select `MegaMan Battle Network 3 Client` during installation).
- A US MegaMan Battle Network 3 Blue Rom. If you have the [MegaMan Battle Network Legacy Collection Vol. 1](https://store.steampowered.com/app/1798010/Mega_Man_Battle_Network_Legacy_Collection_Vol_1/)
on Steam, you can obtain a copy of this ROM from the game's files, see instructions below.
-4
View File
@@ -35,10 +35,6 @@ class NoitaWorld(World):
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
def fill_slot_data(self):
return {name: getattr(self.multiworld, name)[self.player].value for name in self.option_definitions}
+1 -3
View File
@@ -40,8 +40,6 @@ or try restarting your game.
### 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
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?
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.
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.
+2 -1
View File
@@ -10,7 +10,8 @@ As we are using BizHawk, this guide is only applicable to Windows and Linux syst
- Version 2.3.1 and later are supported. Version 2.7 is recommended for stability.
- Detailed installation instructions for BizHawk can be found at the above link.
- Windows users must run the prereq installer first, which can also be found at the above link.
- The built-in Archipelago client, which can be installed [here](https://github.com/ArchipelagoMW/Archipelago/releases).
- The built-in Archipelago client, which can be installed [here](https://github.com/ArchipelagoMW/Archipelago/releases)
(select `Ocarina of Time Client` during installation).
- An Ocarina of Time v1.0 ROM.
## Configuring BizHawk
+389 -37
View File
@@ -1,70 +1,422 @@
# Guide de configuration pour Ocarina of Time Archipelago
# Guide d'installation Archipelago pour Ocarina of Time
## 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
- BizHawk : [Sorties BizHawk 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é.
- 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 la stabilité.
- 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)
(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.
## 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" à
"Lua+LuaInterface". Puis redémarrez EmuHawk. 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**
**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**
- Sous Config > Personnaliser > Avancé, assurez-vous que la case AutoSaveRAM est cochée et cliquez sur le bouton 5s.
Cela réduit la possibilité de perdre des données de sauvegarde en cas de crash de l'émulateur.
- 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.
- 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 ».
- 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".
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 N64, activez "Utiliser le connecteur d'extension". Ceci est nécessaire pour que les états de sauvegarde fonctionnent.
- Allez dans Config > Personnaliser. Basculez vers l'onglet Avancé, puis basculez le Lua Core de "NLua+KopiLua" vers
"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-le. Nouvelles installations**
** des versions plus récentes de BizHawk ont tendance à afficher "Lua+LuaInterface" comme option sélectionnée par défaut mais se chargent toujours **
**"NLua+KopiLua" jusqu'à ce que cette étape soit terminée.**
- Sous Config > Personnaliser > Avancé, assurez-vous que la case pour AutoSaveRAM est cochée et cliquez sur le bouton 5s.
Cela réduit la possibilité de perdre des données de sauvegarde en cas de plantage de l'émulateur.
- Sous Config > Personnaliser, cochez les cases "Exécuter en arrière-plan" et "Accepter la saisie en arrière-plan". Cela vous permettra de
continuer à jouer en arrière-plan, même si une autre fenêtre est sélectionnée.
- 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
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.)
Il est fortement recommandé d'associer les extensions de rom N64 (\*.n64, \*.z64) à l'EmuHawk 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.
Il est fortement recommandé d'associer les extensions de rom N64 (\*.n64, \*.z64) au BizHawk que nous venons d'installer.
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).
## 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.
guide : [Guide de configuration de base de Multiworld](/tutorial/Archipelago/setup/en)
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
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
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.
@@ -1106,30 +1106,21 @@
"parent_map": "MAP_ROUTE120",
"locations": [
"ITEM_ROUTE_120_NUGGET",
"ITEM_ROUTE_120_FULL_HEAL",
"ITEM_ROUTE_120_REVIVE",
"ITEM_ROUTE_120_HYPER_POTION",
"HIDDEN_ITEM_ROUTE_120_RARE_CANDY_2",
"HIDDEN_ITEM_ROUTE_120_ZINC"
],
"events": [],
"exits": [
"REGION_ROUTE120/NORTH",
"REGION_ROUTE120/SOUTH_PONDS",
"REGION_ROUTE121/WEST"
],
"warps": [
"MAP_ROUTE120:0/MAP_ANCIENT_TOMB:0"
]
},
"REGION_ROUTE120/SOUTH_PONDS": {
"parent_map": "MAP_ROUTE120",
"locations": [
"HIDDEN_ITEM_ROUTE_120_RARE_CANDY_2",
"ITEM_ROUTE_120_FULL_HEAL"
],
"events": [],
"exits": [],
"warps": []
},
"REGION_ROUTE121/WEST": {
"parent_map": "MAP_ROUTE121",
"locations": [
@@ -1,15 +1,15 @@
# 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?
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,
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?
@@ -28,7 +28,7 @@ randomizer. Here are some of the more important ones:
- You can have both bikes simultaneously
- You can run or bike (almost) anywhere
- 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
- Many more minor improvements…
@@ -44,7 +44,7 @@ your inventory.
## 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`
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.
## Can I play offline?
+2 -2
View File
@@ -26,8 +26,8 @@ clear it.
## Generating and Patching a Game
1. Create your options file (YAML). You can make one on the
[Pokémon Emerald options page](../../../games/Pokemon%20Emerald/player-options).
1. Create your settings file (YAML). You can make one on the
[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).
This will generate an output file for you. Your patch file will have the `.apemerald` file extension.
3. Open `ArchipelagoLauncher.exe`
-4
View File
@@ -626,10 +626,6 @@ def set_rules(world: "PokemonEmeraldWorld") -> None:
get_entrance("REGION_ROUTE120/NORTH_POND_SHORE -> REGION_ROUTE120/NORTH_POND"),
can_surf
)
set_rule(
get_entrance("REGION_ROUTE120/SOUTH -> REGION_ROUTE120/SOUTH_PONDS"),
can_surf
)
# Route 121
set_rule(
@@ -44,17 +44,13 @@ class TestScorchedSlabPond(PokemonEmeraldTestBase):
class TestSurf(PokemonEmeraldTestBase):
options = {
"npc_gifts": Toggle.option_true,
"hidden_items": Toggle.option_true,
"require_itemfinder": Toggle.option_false
"npc_gifts": Toggle.option_true
}
def test_inaccessible_with_no_surf(self) -> None:
self.assertFalse(self.can_reach_location(location_name_to_label("ITEM_PETALBURG_CITY_ETHER")))
self.assertFalse(self.can_reach_location(location_name_to_label("NPC_GIFT_RECEIVED_SOOTHE_BELL")))
self.assertFalse(self.can_reach_location(location_name_to_label("ITEM_LILYCOVE_CITY_MAX_REPEL")))
self.assertFalse(self.can_reach_location(location_name_to_label("HIDDEN_ITEM_ROUTE_120_RARE_CANDY_2")))
self.assertFalse(self.can_reach_location(location_name_to_label("ITEM_ROUTE_120_FULL_HEAL")))
self.assertFalse(self.can_reach_entrance("REGION_ROUTE118/WATER -> REGION_ROUTE118/EAST"))
self.assertFalse(self.can_reach_entrance("REGION_ROUTE119/UPPER -> REGION_FORTREE_CITY/MAIN"))
self.assertFalse(self.can_reach_entrance("MAP_FORTREE_CITY:3/MAP_FORTREE_CITY_MART:0"))
@@ -64,8 +60,6 @@ class TestSurf(PokemonEmeraldTestBase):
self.assertTrue(self.can_reach_location(location_name_to_label("ITEM_PETALBURG_CITY_ETHER")))
self.assertTrue(self.can_reach_location(location_name_to_label("NPC_GIFT_RECEIVED_SOOTHE_BELL")))
self.assertTrue(self.can_reach_location(location_name_to_label("ITEM_LILYCOVE_CITY_MAX_REPEL")))
self.assertTrue(self.can_reach_location(location_name_to_label("HIDDEN_ITEM_ROUTE_120_RARE_CANDY_2")))
self.assertTrue(self.can_reach_location(location_name_to_label("ITEM_ROUTE_120_FULL_HEAL")))
self.assertTrue(self.can_reach_entrance("REGION_ROUTE118/WATER -> REGION_ROUTE118/EAST"))
self.assertTrue(self.can_reach_entrance("REGION_ROUTE119/UPPER -> REGION_FORTREE_CITY/MAIN"))
self.assertTrue(self.can_reach_entrance("MAP_FORTREE_CITY:3/MAP_FORTREE_CITY_MART:0"))
+7 -10
View File
@@ -281,20 +281,18 @@ class PokemonRedBlueWorld(World):
self.multiworld.itempool.remove(badge)
progitempool.remove(badge)
for _ in range(5):
badgelocs = [
self.multiworld.get_location(loc, self.player) for loc in [
"Pewter Gym - Brock Prize", "Cerulean Gym - Misty Prize",
"Vermilion Gym - Lt. Surge Prize", "Celadon Gym - Erika Prize",
"Fuchsia Gym - Koga Prize", "Saffron Gym - Sabrina Prize",
"Cinnabar Gym - Blaine Prize", "Viridian Gym - Giovanni Prize"
] if self.multiworld.get_location(loc, self.player).item is None]
badgelocs = [self.multiworld.get_location(loc, self.player) for loc in [
"Pewter Gym - Brock Prize", "Cerulean Gym - Misty Prize",
"Vermilion Gym - Lt. Surge Prize", "Celadon Gym - Erika Prize",
"Fuchsia Gym - Koga Prize", "Saffron Gym - Sabrina Prize",
"Cinnabar Gym - Blaine Prize", "Viridian Gym - Giovanni Prize"]]
state = self.multiworld.get_all_state(False)
self.multiworld.random.shuffle(badges)
self.multiworld.random.shuffle(badgelocs)
badgelocs_copy = badgelocs.copy()
# 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)
if len(badges) > 8 - len(badgelocs):
if badges:
for location in badgelocs:
if location.item:
badges.append(location.item)
@@ -304,7 +302,6 @@ class PokemonRedBlueWorld(World):
for location in badgelocs:
if location.item:
fill_locations.remove(location)
progitempool += badges
break
else:
raise FillError(f"Failed to place badges for player {self.player}")
@@ -417,7 +414,7 @@ class PokemonRedBlueWorld(World):
> 7) or (self.multiworld.door_shuffle[self.player] not in ("off", "simple")))):
intervene_move = "Cut"
elif ((not logic.can_learn_hm(test_state, "Flash", self.player)) and self.multiworld.dark_rock_tunnel_logic[self.player]
and (((self.multiworld.accessibility[self.player] != "minimal" or
and (((self.multiworld.accessibility[self.player] != "minimal" and
(self.multiworld.trainersanity[self.player] or self.multiworld.extra_key_items[self.player])) or
self.multiworld.door_shuffle[self.player]))):
intervene_move = "Flash"

Some files were not shown because too many files have changed in this diff Show More