mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-03-14 11:33:47 -07:00
Compare commits
16 Commits
core_lazy_
...
use_self.o
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2524ddc075 | ||
|
|
d545b78803 | ||
|
|
88b1c94eb2 | ||
|
|
7742d5d804 | ||
|
|
d3e148dcc6 | ||
|
|
b5fccde913 | ||
|
|
55e9b0687a | ||
|
|
79e1bf351e | ||
|
|
fcfea9d9aa | ||
|
|
cfc5508f06 | ||
|
|
62cb5f1fc2 | ||
|
|
7e70b16656 | ||
|
|
7b486b3380 | ||
|
|
09cac0a685 | ||
|
|
12c583533d | ||
|
|
c5af28a649 |
@@ -1,5 +0,0 @@
|
||||
[report]
|
||||
exclude_lines =
|
||||
pragma: no cover
|
||||
if TYPE_CHECKING:
|
||||
if typing.TYPE_CHECKING:
|
||||
2
.github/workflows/analyze-modified-files.yml
vendored
2
.github/workflows/analyze-modified-files.yml
vendored
@@ -71,7 +71,7 @@ jobs:
|
||||
continue-on-error: true
|
||||
if: env.diff != '' && matrix.task == 'flake8'
|
||||
run: |
|
||||
flake8 --count --max-complexity=14 --max-doc-length=120 --max-line-length=120 --statistics ${{ env.diff }}
|
||||
flake8 --count --max-complexity=10 --max-doc-length=120 --max-line-length=120 --statistics ${{ env.diff }}
|
||||
|
||||
- name: "mypy: Type check modified files"
|
||||
continue-on-error: true
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Archipelago Unittests" type="tests" factoryName="Unittests">
|
||||
<module name="Archipelago" />
|
||||
<option name="INTERPRETER_OPTIONS" value="" />
|
||||
<option name="PARENT_ENVS" value="true" />
|
||||
<option name="SDK_HOME" value="" />
|
||||
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$" />
|
||||
<option name="IS_MODULE_SDK" value="true" />
|
||||
<option name="ADD_CONTENT_ROOTS" value="true" />
|
||||
<option name="ADD_SOURCE_ROOTS" value="true" />
|
||||
<EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py" />
|
||||
<option name="_new_pattern" value="""" />
|
||||
<option name="_new_additionalArguments" value="""" />
|
||||
<option name="_new_target" value=""$PROJECT_DIR$/test"" />
|
||||
<option name="_new_targetType" value=""PATH"" />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
||||
@@ -1056,6 +1056,9 @@ class Location:
|
||||
|
||||
@property
|
||||
def hint_text(self) -> str:
|
||||
hint_text = getattr(self, "_hint_text", None)
|
||||
if hint_text:
|
||||
return hint_text
|
||||
return "at " + self.name.replace("_", " ").replace("-", " ")
|
||||
|
||||
|
||||
|
||||
@@ -460,7 +460,7 @@ class CommonContext:
|
||||
else:
|
||||
self.update_game(cached_game)
|
||||
if needed_updates:
|
||||
await self.send_msgs([{"cmd": "GetDataPackage", "games": [game_name]} for game_name in needed_updates])
|
||||
await self.send_msgs([{"cmd": "GetDataPackage", "games": list(needed_updates)}])
|
||||
|
||||
def update_game(self, game_package: dict):
|
||||
for item_name, item_id in game_package["item_name_to_id"].items():
|
||||
@@ -477,7 +477,6 @@ class CommonContext:
|
||||
current_cache = Utils.persistent_load().get("datapackage", {}).get("games", {})
|
||||
current_cache.update(data_package["games"])
|
||||
Utils.persistent_store("datapackage", "games", current_cache)
|
||||
logger.info(f"Got new ID/Name DataPackage for {', '.join(data_package['games'])}")
|
||||
for game, game_data in data_package["games"].items():
|
||||
Utils.store_data_package_for_checksum(game, game_data)
|
||||
|
||||
@@ -728,6 +727,7 @@ async def process_server_cmd(ctx: CommonContext, args: dict):
|
||||
await ctx.server_auth(args['password'])
|
||||
|
||||
elif cmd == 'DataPackage':
|
||||
logger.info("Got new ID/Name DataPackage")
|
||||
ctx.consume_network_data_package(args['data'])
|
||||
|
||||
elif cmd == 'ConnectionRefused':
|
||||
|
||||
13
Main.py
13
Main.py
@@ -114,9 +114,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
||||
for _ in range(count):
|
||||
world.push_precollected(world.create_item(item_name, player))
|
||||
|
||||
for item_name, count in getattr(world.worlds[player].options,
|
||||
"start_inventory_from_pool",
|
||||
StartInventoryPool({})).value.items():
|
||||
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.
|
||||
@@ -169,14 +167,11 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
||||
|
||||
# remove starting inventory from pool items.
|
||||
# Because some worlds don't actually create items during create_items this has to be as late as possible.
|
||||
if any(getattr(world.worlds[player].options, "start_inventory_from_pool", None) for player in world.player_ids):
|
||||
if any(getattr(world.worlds[player].options, "start_inventory_from_pool", StartInventoryPool({})).value for player in world.player_ids):
|
||||
new_items: List[Item] = []
|
||||
depletion_pool: Dict[int, Dict[str, int]] = {
|
||||
player: getattr(world.worlds[player].options,
|
||||
"start_inventory_from_pool",
|
||||
StartInventoryPool({})).value.copy()
|
||||
for player in world.player_ids
|
||||
}
|
||||
player: getattr(world.worlds[player].options, "start_inventory_from_pool", StartInventoryPool({})).value.copy()
|
||||
for player in world.player_ids}
|
||||
for player, items in depletion_pool.items():
|
||||
player_world: AutoWorld.World = world.worlds[player]
|
||||
for count in items.values():
|
||||
|
||||
@@ -4,29 +4,14 @@ import subprocess
|
||||
import multiprocessing
|
||||
import warnings
|
||||
|
||||
local_dir = os.path.dirname(__file__)
|
||||
requirements_files = {os.path.join(local_dir, 'requirements.txt')}
|
||||
|
||||
if sys.version_info < (3, 8, 6):
|
||||
raise RuntimeError("Incompatible Python Version. 3.8.7+ is supported.")
|
||||
|
||||
# don't run update if environment is frozen/compiled or if not the parent process (skip in subprocess)
|
||||
_skip_update = bool(getattr(sys, "frozen", False) or multiprocessing.parent_process())
|
||||
update_ran = _skip_update
|
||||
|
||||
|
||||
class RequirementsSet(set):
|
||||
def add(self, e):
|
||||
global update_ran
|
||||
update_ran &= _skip_update
|
||||
super().add(e)
|
||||
|
||||
def update(self, *s):
|
||||
global update_ran
|
||||
update_ran &= _skip_update
|
||||
super().update(*s)
|
||||
|
||||
|
||||
local_dir = os.path.dirname(__file__)
|
||||
requirements_files = RequirementsSet((os.path.join(local_dir, 'requirements.txt'),))
|
||||
update_ran = getattr(sys, "frozen", False) or multiprocessing.parent_process()
|
||||
|
||||
if not update_ran:
|
||||
for entry in os.scandir(os.path.join(local_dir, "worlds")):
|
||||
|
||||
@@ -2210,24 +2210,25 @@ def parse_args() -> argparse.Namespace:
|
||||
|
||||
async def auto_shutdown(ctx, to_cancel=None):
|
||||
await asyncio.sleep(ctx.auto_shutdown)
|
||||
|
||||
def inactivity_shutdown():
|
||||
ctx.server.ws_server.close()
|
||||
ctx.exit_event.set()
|
||||
if to_cancel:
|
||||
for task in to_cancel:
|
||||
task.cancel()
|
||||
logging.info("Shutting down due to inactivity.")
|
||||
|
||||
while not ctx.exit_event.is_set():
|
||||
if not ctx.client_activity_timers.values():
|
||||
inactivity_shutdown()
|
||||
ctx.server.ws_server.close()
|
||||
ctx.exit_event.set()
|
||||
if to_cancel:
|
||||
for task in to_cancel:
|
||||
task.cancel()
|
||||
logging.info("Shutting down due to inactivity.")
|
||||
else:
|
||||
newest_activity = max(ctx.client_activity_timers.values())
|
||||
delta = datetime.datetime.now(datetime.timezone.utc) - newest_activity
|
||||
seconds = ctx.auto_shutdown - delta.total_seconds()
|
||||
if seconds < 0:
|
||||
inactivity_shutdown()
|
||||
ctx.server.ws_server.close()
|
||||
ctx.exit_event.set()
|
||||
if to_cancel:
|
||||
for task in to_cancel:
|
||||
task.cancel()
|
||||
logging.info("Shutting down due to inactivity.")
|
||||
else:
|
||||
await asyncio.sleep(seconds)
|
||||
|
||||
|
||||
@@ -58,7 +58,6 @@ Currently, the following games are supported:
|
||||
* Heretic
|
||||
* Landstalker: The Treasures of King Nole
|
||||
* Final Fantasy Mystic Quest
|
||||
* TUNIC
|
||||
|
||||
For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/).
|
||||
Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled
|
||||
|
||||
@@ -20,8 +20,8 @@ def generate_api():
|
||||
race = False
|
||||
meta_options_source = {}
|
||||
if 'file' in request.files:
|
||||
files = request.files.getlist('file')
|
||||
options = get_yaml_data(files)
|
||||
file = request.files['file']
|
||||
options = get_yaml_data(file)
|
||||
if isinstance(options, Markup):
|
||||
return {"text": options.striptags()}, 400
|
||||
if isinstance(options, str):
|
||||
|
||||
@@ -69,8 +69,8 @@
|
||||
</td>
|
||||
<td>
|
||||
<select name="collect_mode" id="collect_mode">
|
||||
<option value="auto">Automatic on goal completion</option>
|
||||
<option value="goal">Allow !collect after goal completion</option>
|
||||
<option value="auto">Automatic on goal completion</option>
|
||||
<option value="auto-enabled">
|
||||
Automatic on goal completion and manual !collect
|
||||
</option>
|
||||
@@ -93,9 +93,9 @@
|
||||
{% if race -%}
|
||||
<option value="disabled">Disabled in Race mode</option>
|
||||
{%- else -%}
|
||||
<option value="disabled">Disabled</option>
|
||||
<option value="goal">Allow !remaining after goal completion</option>
|
||||
<option value="enabled">Manual !remaining</option>
|
||||
<option value="disabled">Disabled</option>
|
||||
{%- endif -%}
|
||||
</select>
|
||||
</td>
|
||||
@@ -185,12 +185,12 @@ Warning: playthrough can take a significant amount of time for larger multiworld
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<input type="checkbox" id="plando_items" name="plando_items" value="items">
|
||||
<label for="plando_items">Items</label><br>
|
||||
|
||||
<input type="checkbox" id="plando_bosses" name="plando_bosses" value="bosses" checked>
|
||||
<label for="plando_bosses">Bosses</label><br>
|
||||
|
||||
<input type="checkbox" id="plando_items" name="plando_items" value="items" checked>
|
||||
<label for="plando_items">Items</label><br>
|
||||
|
||||
<input type="checkbox" id="plando_connections" name="plando_connections" value="connections" checked>
|
||||
<label for="plando_connections">Connections</label><br>
|
||||
|
||||
|
||||
@@ -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,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(),
|
||||
|
||||
@@ -11,14 +11,11 @@ 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
|
||||
|
||||
@@ -26,15 +23,6 @@ banned_extensions = (".sfc", ".z64", ".n64", ".nes", ".smc", ".sms", ".gb", ".gb
|
||||
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)
|
||||
@@ -49,8 +37,6 @@ def banned_file(filename: str) -> bool:
|
||||
|
||||
|
||||
def process_multidata(compressed_multidata, files={}):
|
||||
game_data: GamesPackage
|
||||
|
||||
decompressed_multidata = MultiServer.Context.decompress(compressed_multidata)
|
||||
|
||||
slots: typing.Set[Slot] = set()
|
||||
@@ -59,19 +45,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,15 +64,14 @@ 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"]
|
||||
|
||||
505
ZillionClient.py
505
ZillionClient.py
@@ -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, cast
|
||||
|
||||
import Utils # noqa: E402
|
||||
# CommonClient import first to trigger ModuleUpdater
|
||||
from CommonClient import CommonContext, server_loop, gui_enabled, \
|
||||
ClientCommandProcessor, logger, get_base_parser
|
||||
from NetUtils import ClientStatus
|
||||
import Utils
|
||||
from Utils import async_start
|
||||
|
||||
import colorama
|
||||
|
||||
from zilliandomizer.zri.memory import Memory
|
||||
from zilliandomizer.zri import events
|
||||
from zilliandomizer.utils.loc_name_maps import id_to_loc
|
||||
from zilliandomizer.options import Chars
|
||||
from zilliandomizer.patch import RescueInfo
|
||||
|
||||
from worlds.zillion.id_maps import make_id_to_others
|
||||
from worlds.zillion.config import base_id, zillion_map
|
||||
|
||||
|
||||
class ZillionCommandProcessor(ClientCommandProcessor):
|
||||
ctx: "ZillionContext"
|
||||
|
||||
def _cmd_sms(self) -> None:
|
||||
""" Tell the client that Zillion is running in RetroArch. """
|
||||
logger.info("ready to look for game")
|
||||
self.ctx.look_for_retroarch.set()
|
||||
|
||||
def _cmd_map(self) -> None:
|
||||
""" Toggle view of the map tracker. """
|
||||
self.ctx.ui_toggle_map()
|
||||
|
||||
|
||||
class ToggleCallback(Protocol):
|
||||
def __call__(self) -> None: ...
|
||||
|
||||
|
||||
class SetRoomCallback(Protocol):
|
||||
def __call__(self, rooms: List[List[int]]) -> None: ...
|
||||
|
||||
|
||||
class ZillionContext(CommonContext):
|
||||
game = "Zillion"
|
||||
command_processor = ZillionCommandProcessor
|
||||
items_handling = 1 # receive items from other players
|
||||
|
||||
known_name: Optional[str]
|
||||
""" This is almost the same as `auth` except `auth` is reset to `None` when server disconnects, and this isn't. """
|
||||
|
||||
from_game: "asyncio.Queue[events.EventFromGame]"
|
||||
to_game: "asyncio.Queue[events.EventToGame]"
|
||||
ap_local_count: int
|
||||
""" local checks watched by server """
|
||||
next_item: int
|
||||
""" index in `items_received` """
|
||||
ap_id_to_name: Dict[int, str]
|
||||
ap_id_to_zz_id: Dict[int, int]
|
||||
start_char: Chars = "JJ"
|
||||
rescues: Dict[int, RescueInfo] = {}
|
||||
loc_mem_to_id: Dict[int, int] = {}
|
||||
got_room_info: asyncio.Event
|
||||
""" flag for connected to server """
|
||||
got_slot_data: asyncio.Event
|
||||
""" serves as a flag for whether I am logged in to the server """
|
||||
|
||||
look_for_retroarch: asyncio.Event
|
||||
"""
|
||||
There is a bug in Python in Windows
|
||||
https://github.com/python/cpython/issues/91227
|
||||
that makes it so if I look for RetroArch before it's ready,
|
||||
it breaks the asyncio udp transport system.
|
||||
|
||||
As a workaround, we don't look for RetroArch until this event is set.
|
||||
"""
|
||||
|
||||
ui_toggle_map: ToggleCallback
|
||||
ui_set_rooms: SetRoomCallback
|
||||
""" parameter is y 16 x 8 numbers to show in each room """
|
||||
|
||||
def __init__(self,
|
||||
server_address: str,
|
||||
password: str) -> None:
|
||||
super().__init__(server_address, password)
|
||||
self.known_name = None
|
||||
self.from_game = asyncio.Queue()
|
||||
self.to_game = asyncio.Queue()
|
||||
self.got_room_info = asyncio.Event()
|
||||
self.got_slot_data = asyncio.Event()
|
||||
self.ui_toggle_map = lambda: None
|
||||
self.ui_set_rooms = lambda rooms: None
|
||||
|
||||
self.look_for_retroarch = asyncio.Event()
|
||||
if platform.system() != "Windows":
|
||||
# asyncio udp bug is only on Windows
|
||||
self.look_for_retroarch.set()
|
||||
|
||||
self.reset_game_state()
|
||||
|
||||
def reset_game_state(self) -> None:
|
||||
for _ in range(self.from_game.qsize()):
|
||||
self.from_game.get_nowait()
|
||||
for _ in range(self.to_game.qsize()):
|
||||
self.to_game.get_nowait()
|
||||
self.got_slot_data.clear()
|
||||
|
||||
self.ap_local_count = 0
|
||||
self.next_item = 0
|
||||
self.ap_id_to_name = {}
|
||||
self.ap_id_to_zz_id = {}
|
||||
self.rescues = {}
|
||||
self.loc_mem_to_id = {}
|
||||
|
||||
self.locations_checked.clear()
|
||||
self.missing_locations.clear()
|
||||
self.checked_locations.clear()
|
||||
self.finished_game = False
|
||||
self.items_received.clear()
|
||||
|
||||
# override
|
||||
def on_deathlink(self, data: Dict[str, Any]) -> None:
|
||||
self.to_game.put_nowait(events.DeathEventToGame())
|
||||
return super().on_deathlink(data)
|
||||
|
||||
# override
|
||||
async def server_auth(self, password_requested: bool = False) -> None:
|
||||
if password_requested and not self.password:
|
||||
await super().server_auth(password_requested)
|
||||
if not self.auth:
|
||||
logger.info('waiting for connection to game...')
|
||||
return
|
||||
logger.info("logging in to server...")
|
||||
await self.send_connect()
|
||||
|
||||
# override
|
||||
def run_gui(self) -> None:
|
||||
from kvui import GameManager
|
||||
from kivy.core.text import Label as CoreLabel
|
||||
from kivy.graphics import Ellipse, Color, Rectangle
|
||||
from kivy.uix.layout import Layout
|
||||
from kivy.uix.widget import Widget
|
||||
|
||||
class ZillionManager(GameManager):
|
||||
logging_pairs = [
|
||||
("Client", "Archipelago")
|
||||
]
|
||||
base_title = "Archipelago Zillion Client"
|
||||
|
||||
class MapPanel(Widget):
|
||||
MAP_WIDTH: ClassVar[int] = 281
|
||||
|
||||
_number_textures: List[Any] = []
|
||||
rooms: List[List[int]] = []
|
||||
|
||||
def __init__(self, **kwargs: Any) -> None:
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self.rooms = [[0 for _ in range(8)] for _ in range(16)]
|
||||
|
||||
self._make_numbers()
|
||||
self.update_map()
|
||||
|
||||
self.bind(pos=self.update_map)
|
||||
# self.bind(size=self.update_bg)
|
||||
|
||||
def _make_numbers(self) -> None:
|
||||
self._number_textures = []
|
||||
for n in range(10):
|
||||
label = CoreLabel(text=str(n), font_size=22, color=(0.1, 0.9, 0, 1))
|
||||
label.refresh()
|
||||
self._number_textures.append(label.texture)
|
||||
|
||||
def update_map(self, *args: Any) -> None:
|
||||
self.canvas.clear()
|
||||
|
||||
with self.canvas:
|
||||
Color(1, 1, 1, 1)
|
||||
Rectangle(source=zillion_map,
|
||||
pos=self.pos,
|
||||
size=(ZillionManager.MapPanel.MAP_WIDTH,
|
||||
int(ZillionManager.MapPanel.MAP_WIDTH * 1.456))) # aspect ratio of that image
|
||||
for y in range(16):
|
||||
for x in range(8):
|
||||
num = self.rooms[15 - y][x]
|
||||
if num > 0:
|
||||
Color(0, 0, 0, 0.4)
|
||||
pos = [self.pos[0] + 17 + x * 32, self.pos[1] + 14 + y * 24]
|
||||
Ellipse(size=[22, 22], pos=pos)
|
||||
Color(1, 1, 1, 1)
|
||||
pos = [self.pos[0] + 22 + x * 32, self.pos[1] + 12 + y * 24]
|
||||
num_texture = self._number_textures[num]
|
||||
Rectangle(texture=num_texture, size=num_texture.size, pos=pos)
|
||||
|
||||
def build(self) -> Layout:
|
||||
container = super().build()
|
||||
self.map_widget = ZillionManager.MapPanel(size_hint_x=None, width=0)
|
||||
self.main_area_container.add_widget(self.map_widget)
|
||||
return container
|
||||
|
||||
def toggle_map_width(self) -> None:
|
||||
if self.map_widget.width == 0:
|
||||
self.map_widget.width = ZillionManager.MapPanel.MAP_WIDTH
|
||||
else:
|
||||
self.map_widget.width = 0
|
||||
self.container.do_layout()
|
||||
|
||||
def set_rooms(self, rooms: List[List[int]]) -> None:
|
||||
self.map_widget.rooms = rooms
|
||||
self.map_widget.update_map()
|
||||
|
||||
self.ui = ZillionManager(self)
|
||||
self.ui_toggle_map = lambda: self.ui.toggle_map_width()
|
||||
self.ui_set_rooms = lambda rooms: self.ui.set_rooms(rooms)
|
||||
run_co: Coroutine[Any, Any, None] = self.ui.async_run()
|
||||
self.ui_task = asyncio.create_task(run_co, name="UI")
|
||||
|
||||
def on_package(self, cmd: str, args: Dict[str, Any]) -> None:
|
||||
self.room_item_numbers_to_ui()
|
||||
if cmd == "Connected":
|
||||
logger.info("logged in to Archipelago server")
|
||||
if "slot_data" not in args:
|
||||
logger.warn("`Connected` packet missing `slot_data`")
|
||||
return
|
||||
slot_data = args["slot_data"]
|
||||
|
||||
if "start_char" not in slot_data:
|
||||
logger.warn("invalid Zillion `Connected` packet, `slot_data` missing `start_char`")
|
||||
return
|
||||
self.start_char = slot_data['start_char']
|
||||
if self.start_char not in {"Apple", "Champ", "JJ"}:
|
||||
logger.warn("invalid Zillion `Connected` packet, "
|
||||
f"`slot_data` `start_char` has invalid value: {self.start_char}")
|
||||
|
||||
if "rescues" not in slot_data:
|
||||
logger.warn("invalid Zillion `Connected` packet, `slot_data` missing `rescues`")
|
||||
return
|
||||
rescues = slot_data["rescues"]
|
||||
self.rescues = {}
|
||||
for rescue_id, json_info in rescues.items():
|
||||
assert rescue_id in ("0", "1"), f"invalid rescue_id in Zillion slot_data: {rescue_id}"
|
||||
# TODO: just take start_char out of the RescueInfo so there's no opportunity for a mismatch?
|
||||
assert json_info["start_char"] == self.start_char, \
|
||||
f'mismatch in Zillion slot data: {json_info["start_char"]} {self.start_char}'
|
||||
ri = RescueInfo(json_info["start_char"],
|
||||
json_info["room_code"],
|
||||
json_info["mask"])
|
||||
self.rescues[0 if rescue_id == "0" else 1] = ri
|
||||
|
||||
if "loc_mem_to_id" not in slot_data:
|
||||
logger.warn("invalid Zillion `Connected` packet, `slot_data` missing `loc_mem_to_id`")
|
||||
return
|
||||
loc_mem_to_id = slot_data["loc_mem_to_id"]
|
||||
self.loc_mem_to_id = {}
|
||||
for mem_str, id_str in loc_mem_to_id.items():
|
||||
mem = int(mem_str)
|
||||
id_ = int(id_str)
|
||||
room_i = mem // 256
|
||||
assert 0 <= room_i < 74
|
||||
assert id_ in id_to_loc
|
||||
self.loc_mem_to_id[mem] = id_
|
||||
|
||||
if len(self.loc_mem_to_id) != 394:
|
||||
logger.warn("invalid Zillion `Connected` packet, "
|
||||
f"`slot_data` missing locations in `loc_mem_to_id` - len {len(self.loc_mem_to_id)}")
|
||||
|
||||
self.got_slot_data.set()
|
||||
|
||||
payload = {
|
||||
"cmd": "Get",
|
||||
"keys": [f"zillion-{self.auth}-doors"]
|
||||
}
|
||||
async_start(self.send_msgs([payload]))
|
||||
elif cmd == "Retrieved":
|
||||
if "keys" not in args:
|
||||
logger.warning(f"invalid Retrieved packet to ZillionClient: {args}")
|
||||
return
|
||||
keys = cast(Dict[str, Optional[str]], args["keys"])
|
||||
doors_b64 = keys.get(f"zillion-{self.auth}-doors", None)
|
||||
if doors_b64:
|
||||
logger.info("received door data from server")
|
||||
doors = base64.b64decode(doors_b64)
|
||||
self.to_game.put_nowait(events.DoorEventToGame(doors))
|
||||
elif cmd == "RoomInfo":
|
||||
self.seed_name = args["seed_name"]
|
||||
self.got_room_info.set()
|
||||
|
||||
def room_item_numbers_to_ui(self) -> None:
|
||||
rooms = [[0 for _ in range(8)] for _ in range(16)]
|
||||
for loc_id in self.missing_locations:
|
||||
loc_id_small = loc_id - base_id
|
||||
loc_name = id_to_loc[loc_id_small]
|
||||
y = ord(loc_name[0]) - 65
|
||||
x = ord(loc_name[2]) - 49
|
||||
if y == 9 and x == 5:
|
||||
# don't show main computer in numbers
|
||||
continue
|
||||
assert (0 <= y < 16) and (0 <= x < 8), f"invalid index from location name {loc_name}"
|
||||
rooms[y][x] += 1
|
||||
# TODO: also add locations with locals lost from loading save state or reset
|
||||
self.ui_set_rooms(rooms)
|
||||
|
||||
def process_from_game_queue(self) -> None:
|
||||
if self.from_game.qsize():
|
||||
event_from_game = self.from_game.get_nowait()
|
||||
if isinstance(event_from_game, events.AcquireLocationEventFromGame):
|
||||
server_id = event_from_game.id + base_id
|
||||
loc_name = id_to_loc[event_from_game.id]
|
||||
self.locations_checked.add(server_id)
|
||||
if server_id in self.missing_locations:
|
||||
self.ap_local_count += 1
|
||||
n_locations = len(self.missing_locations) + len(self.checked_locations) - 1 # -1 to ignore win
|
||||
logger.info(f'New Check: {loc_name} ({self.ap_local_count}/{n_locations})')
|
||||
async_start(self.send_msgs([
|
||||
{"cmd": 'LocationChecks', "locations": [server_id]}
|
||||
]))
|
||||
else:
|
||||
# This will happen a lot in Zillion,
|
||||
# because all the key words are local and unwatched by the server.
|
||||
logger.debug(f"DEBUG: {loc_name} not in missing")
|
||||
elif isinstance(event_from_game, events.DeathEventFromGame):
|
||||
async_start(self.send_death())
|
||||
elif isinstance(event_from_game, events.WinEventFromGame):
|
||||
if not self.finished_game:
|
||||
async_start(self.send_msgs([
|
||||
{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}
|
||||
]))
|
||||
self.finished_game = True
|
||||
elif isinstance(event_from_game, events.DoorEventFromGame):
|
||||
if self.auth:
|
||||
doors_b64 = base64.b64encode(event_from_game.doors).decode()
|
||||
payload = {
|
||||
"cmd": "Set",
|
||||
"key": f"zillion-{self.auth}-doors",
|
||||
"operations": [{"operation": "replace", "value": doors_b64}]
|
||||
}
|
||||
async_start(self.send_msgs([payload]))
|
||||
else:
|
||||
logger.warning(f"WARNING: unhandled event from game {event_from_game}")
|
||||
|
||||
def process_items_received(self) -> None:
|
||||
if len(self.items_received) > self.next_item:
|
||||
zz_item_ids = [self.ap_id_to_zz_id[item.item] for item in self.items_received]
|
||||
for index in range(self.next_item, len(self.items_received)):
|
||||
ap_id = self.items_received[index].item
|
||||
from_name = self.player_names[self.items_received[index].player]
|
||||
# TODO: colors in this text, like sni client?
|
||||
logger.info(f'received {self.ap_id_to_name[ap_id]} from {from_name}')
|
||||
self.to_game.put_nowait(
|
||||
events.ItemEventToGame(zz_item_ids)
|
||||
)
|
||||
self.next_item = len(self.items_received)
|
||||
|
||||
|
||||
def name_seed_from_ram(data: bytes) -> Tuple[str, str]:
|
||||
""" returns player name, and end of seed string """
|
||||
if len(data) == 0:
|
||||
# no connection to game
|
||||
return "", "xxx"
|
||||
null_index = data.find(b'\x00')
|
||||
if null_index == -1:
|
||||
logger.warning(f"invalid game id in rom {repr(data)}")
|
||||
null_index = len(data)
|
||||
name = data[:null_index].decode()
|
||||
null_index_2 = data.find(b'\x00', null_index + 1)
|
||||
if null_index_2 == -1:
|
||||
null_index_2 = len(data)
|
||||
seed_name = data[null_index + 1:null_index_2].decode()
|
||||
|
||||
return name, seed_name
|
||||
|
||||
|
||||
async def zillion_sync_task(ctx: ZillionContext) -> None:
|
||||
logger.info("started zillion sync task")
|
||||
|
||||
# to work around the Python bug where we can't check for RetroArch
|
||||
if not ctx.look_for_retroarch.is_set():
|
||||
logger.info("Start Zillion in RetroArch, then use the /sms command to connect to it.")
|
||||
await asyncio.wait((
|
||||
asyncio.create_task(ctx.look_for_retroarch.wait()),
|
||||
asyncio.create_task(ctx.exit_event.wait())
|
||||
), return_when=asyncio.FIRST_COMPLETED)
|
||||
|
||||
last_log = ""
|
||||
|
||||
def log_no_spam(msg: str) -> None:
|
||||
nonlocal last_log
|
||||
if msg != last_log:
|
||||
last_log = msg
|
||||
logger.info(msg)
|
||||
|
||||
# to only show this message once per client run
|
||||
help_message_shown = False
|
||||
|
||||
with Memory(ctx.from_game, ctx.to_game) as memory:
|
||||
while not ctx.exit_event.is_set():
|
||||
ram = await memory.read()
|
||||
game_id = memory.get_rom_to_ram_data(ram)
|
||||
name, seed_end = name_seed_from_ram(game_id)
|
||||
if len(name):
|
||||
if name == ctx.known_name:
|
||||
ctx.auth = name
|
||||
# this is the name we know
|
||||
if ctx.server and ctx.server.socket: # type: ignore
|
||||
if ctx.got_room_info.is_set():
|
||||
if ctx.seed_name and ctx.seed_name.endswith(seed_end):
|
||||
# correct seed
|
||||
if memory.have_generation_info():
|
||||
log_no_spam("everything connected")
|
||||
await memory.process_ram(ram)
|
||||
ctx.process_from_game_queue()
|
||||
ctx.process_items_received()
|
||||
else: # no generation info
|
||||
if ctx.got_slot_data.is_set():
|
||||
memory.set_generation_info(ctx.rescues, ctx.loc_mem_to_id)
|
||||
ctx.ap_id_to_name, ctx.ap_id_to_zz_id, _ap_id_to_zz_item = \
|
||||
make_id_to_others(ctx.start_char)
|
||||
ctx.next_item = 0
|
||||
ctx.ap_local_count = len(ctx.checked_locations)
|
||||
else: # no slot data yet
|
||||
async_start(ctx.send_connect())
|
||||
log_no_spam("logging in to server...")
|
||||
await asyncio.wait((
|
||||
asyncio.create_task(ctx.got_slot_data.wait()),
|
||||
asyncio.create_task(ctx.exit_event.wait()),
|
||||
asyncio.create_task(asyncio.sleep(6))
|
||||
), return_when=asyncio.FIRST_COMPLETED) # to not spam connect packets
|
||||
else: # not correct seed name
|
||||
log_no_spam("incorrect seed - did you mix up roms?")
|
||||
else: # no room info
|
||||
# If we get here, it looks like `RoomInfo` packet got lost
|
||||
log_no_spam("waiting for room info from server...")
|
||||
else: # server not connected
|
||||
log_no_spam("waiting for server connection...")
|
||||
else: # new game
|
||||
log_no_spam("connected to new game")
|
||||
await ctx.disconnect()
|
||||
ctx.reset_server_state()
|
||||
ctx.seed_name = None
|
||||
ctx.got_room_info.clear()
|
||||
ctx.reset_game_state()
|
||||
memory.reset_game_state()
|
||||
|
||||
ctx.auth = name
|
||||
ctx.known_name = name
|
||||
async_start(ctx.connect())
|
||||
await asyncio.wait((
|
||||
asyncio.create_task(ctx.got_room_info.wait()),
|
||||
asyncio.create_task(ctx.exit_event.wait()),
|
||||
asyncio.create_task(asyncio.sleep(6))
|
||||
), return_when=asyncio.FIRST_COMPLETED)
|
||||
else: # no name found in game
|
||||
if not help_message_shown:
|
||||
logger.info('In RetroArch, make sure "Settings > Network > Network Commands" is on.')
|
||||
help_message_shown = True
|
||||
log_no_spam("looking for connection to game...")
|
||||
await asyncio.sleep(0.3)
|
||||
|
||||
await asyncio.sleep(0.09375)
|
||||
logger.info("zillion sync task ending")
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
parser = get_base_parser()
|
||||
parser.add_argument('diff_file', default="", type=str, nargs="?",
|
||||
help='Path to a .apzl Archipelago Binary Patch file')
|
||||
# SNI parser.add_argument('--loglevel', default='info', choices=['debug', 'info', 'warning', 'error', 'critical'])
|
||||
args = parser.parse_args()
|
||||
print(args)
|
||||
|
||||
if args.diff_file:
|
||||
import Patch
|
||||
logger.info("patch file was supplied - creating sms rom...")
|
||||
meta, rom_file = Patch.create_rom_file(args.diff_file)
|
||||
if "server" in meta:
|
||||
args.connect = meta["server"]
|
||||
logger.info(f"wrote rom file to {rom_file}")
|
||||
|
||||
ctx = ZillionContext(args.connect, args.password)
|
||||
if ctx.server_task is None:
|
||||
ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop")
|
||||
|
||||
if gui_enabled:
|
||||
ctx.run_gui()
|
||||
ctx.run_cli()
|
||||
|
||||
sync_task = asyncio.create_task(zillion_sync_task(ctx))
|
||||
|
||||
await ctx.exit_event.wait()
|
||||
|
||||
ctx.server_address = None
|
||||
logger.debug("waiting for sync task to end")
|
||||
await sync_task
|
||||
logger.debug("sync task ended")
|
||||
await ctx.shutdown()
|
||||
|
||||
from worlds.zillion.client import launch # noqa: E402
|
||||
|
||||
if __name__ == "__main__":
|
||||
Utils.init_logging("ZillionClient", exception_logger="Client")
|
||||
launch()
|
||||
|
||||
colorama.init()
|
||||
asyncio.run(main())
|
||||
colorama.deinit()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -164,9 +164,6 @@
|
||||
# The Legend of Zelda (1)
|
||||
/worlds/tloz/ @Rosalie-A @t3hf1gm3nt
|
||||
|
||||
# TUNIC
|
||||
/worlds/tunic/ @silent-destroyer
|
||||
|
||||
# Undertale
|
||||
/worlds/undertale/ @jonloveslegos
|
||||
|
||||
|
||||
@@ -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.
|
||||
@@ -675,8 +674,8 @@ Tags are represented as a list of strings, the common Client tags follow:
|
||||
### DeathLink
|
||||
A special kind of Bounce packet that can be supported by any AP game. It targets the tag "DeathLink" and carries the following data:
|
||||
|
||||
| Name | Type | Notes |
|
||||
|--------|-------|--------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| time | float | Unix Time Stamp of time of death. |
|
||||
| cause | str | Optional. Text to explain the cause of death. When provided, or checked, this should contain the player name, ex. "Berserker was run over by a train." |
|
||||
| source | str | Name of the player who first died. Can be a slot name, but can also be a name from within a multiplayer game. |
|
||||
| Name | Type | Notes |
|
||||
| ---- | ---- | ---- |
|
||||
| time | float | Unix Time Stamp of time of death. |
|
||||
| cause | str | Optional. Text to explain the cause of death, ex. "Berserker was run over by a train." |
|
||||
| source | str | Name of the player who first died. Can be a slot name, but can also be a name from within a multiplayer game. |
|
||||
|
||||
@@ -197,7 +197,7 @@ begin
|
||||
begin
|
||||
// Is the installed version at least the packaged one ?
|
||||
Log('VC Redist x64 Version : found ' + strVersion);
|
||||
Result := (CompareStr(strVersion, 'v14.38.33130') < 0);
|
||||
Result := (CompareStr(strVersion, 'v14.32.31332') < 0);
|
||||
end
|
||||
else
|
||||
begin
|
||||
|
||||
@@ -4,7 +4,7 @@ PyYAML>=6.0.1
|
||||
jellyfish>=1.0.3
|
||||
jinja2>=3.1.2
|
||||
schema>=0.7.5
|
||||
kivy>=2.3.0
|
||||
kivy>=2.2.1
|
||||
bsdiff4>=1.2.4
|
||||
platformdirs>=4.0.0
|
||||
certifi>=2023.11.17
|
||||
|
||||
@@ -597,8 +597,8 @@ class ServerOptions(Group):
|
||||
disable_item_cheat: Union[DisableItemCheat, bool] = False
|
||||
location_check_points: LocationCheckPoints = LocationCheckPoints(1)
|
||||
hint_cost: HintCost = HintCost(10)
|
||||
release_mode: ReleaseMode = ReleaseMode("auto")
|
||||
collect_mode: CollectMode = CollectMode("auto")
|
||||
release_mode: ReleaseMode = ReleaseMode("goal")
|
||||
collect_mode: CollectMode = CollectMode("goal")
|
||||
remaining_mode: RemainingMode = RemainingMode("goal")
|
||||
auto_shutdown: AutoShutdown = AutoShutdown(0)
|
||||
compatibility: Compatibility = Compatibility(2)
|
||||
@@ -673,7 +673,7 @@ class GeneratorOptions(Group):
|
||||
spoiler: Spoiler = Spoiler(3)
|
||||
glitch_triforce_room: GlitchTriforceRoom = GlitchTriforceRoom(1) # why is this here?
|
||||
race: Race = Race(0)
|
||||
plando_options: PlandoOptions = PlandoOptions("bosses, connections, texts")
|
||||
plando_options: PlandoOptions = PlandoOptions("bosses")
|
||||
|
||||
|
||||
class SNIOptions(Group):
|
||||
|
||||
15
setup.py
15
setup.py
@@ -54,6 +54,7 @@ if __name__ == "__main__":
|
||||
# TODO: move stuff to not require this
|
||||
import ModuleUpdate
|
||||
ModuleUpdate.update(yes="--yes" in sys.argv or "-y" in sys.argv)
|
||||
ModuleUpdate.update_ran = False # restore for later
|
||||
|
||||
from worlds.LauncherComponents import components, icon_paths
|
||||
from Utils import version_tuple, is_windows, is_linux
|
||||
@@ -75,6 +76,7 @@ non_apworlds: set = {
|
||||
"Ocarina of Time",
|
||||
"Overcooked! 2",
|
||||
"Raft",
|
||||
"Secret of Evermore",
|
||||
"Slay the Spire",
|
||||
"Sudoku",
|
||||
"Super Mario 64",
|
||||
@@ -303,6 +305,7 @@ class BuildExeCommand(cx_Freeze.command.build_exe.BuildEXE):
|
||||
print(f"Outputting to: {self.buildfolder}")
|
||||
os.makedirs(self.buildfolder, exist_ok=True)
|
||||
import ModuleUpdate
|
||||
ModuleUpdate.requirements_files.add(os.path.join("WebHostLib", "requirements.txt"))
|
||||
ModuleUpdate.update(yes=self.yes)
|
||||
|
||||
# auto-build cython modules
|
||||
@@ -349,18 +352,6 @@ class BuildExeCommand(cx_Freeze.command.build_exe.BuildEXE):
|
||||
for folder in sdl2.dep_bins + glew.dep_bins:
|
||||
shutil.copytree(folder, self.libfolder, dirs_exist_ok=True)
|
||||
print(f"copying {folder} -> {self.libfolder}")
|
||||
# windows needs Visual Studio C++ Redistributable
|
||||
# Installer works for x64 and arm64
|
||||
print("Downloading VC Redist")
|
||||
import certifi
|
||||
import ssl
|
||||
context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH, cafile=certifi.where())
|
||||
with urllib.request.urlopen(r"https://aka.ms/vs/17/release/vc_redist.x64.exe",
|
||||
context=context) as download:
|
||||
vc_redist = download.read()
|
||||
print(f"Download complete, {len(vc_redist) / 1024 / 1024:.2f} MBytes downloaded.", )
|
||||
with open("VC_redist.x64.exe", "wb") as vc_file:
|
||||
vc_file.write(vc_redist)
|
||||
|
||||
for data in self.extra_data:
|
||||
self.installfile(Path(data))
|
||||
|
||||
@@ -285,7 +285,7 @@ class WorldTestBase(unittest.TestCase):
|
||||
if not (self.run_default_tests and self.constructed):
|
||||
return
|
||||
with self.subTest("Game", game=self.game):
|
||||
excluded = self.multiworld.worlds[1].options.exclude_locations.value
|
||||
excluded = self.multiworld.exclude_locations[1].value
|
||||
state = self.multiworld.get_all_state(False)
|
||||
for location in self.multiworld.get_locations():
|
||||
if location.name not in excluded:
|
||||
|
||||
@@ -1,127 +0,0 @@
|
||||
import time
|
||||
|
||||
|
||||
class TimeIt:
|
||||
def __init__(self, name: str, time_logger=None):
|
||||
self.name = name
|
||||
self.logger = time_logger
|
||||
self.timer = None
|
||||
self.end_timer = None
|
||||
|
||||
def __enter__(self):
|
||||
self.timer = time.perf_counter()
|
||||
return self
|
||||
|
||||
@property
|
||||
def dif(self):
|
||||
return self.end_timer - self.timer
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
if not self.end_timer:
|
||||
self.end_timer = time.perf_counter()
|
||||
if self.logger:
|
||||
self.logger.info(f"{self.dif:.4f} seconds in {self.name}.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import argparse
|
||||
import logging
|
||||
import gc
|
||||
import collections
|
||||
import typing
|
||||
|
||||
# makes this module runnable from its folder.
|
||||
import sys
|
||||
import os
|
||||
sys.path.remove(os.path.dirname(__file__))
|
||||
new_home = os.path.normpath(os.path.join(os.path.dirname(__file__), os.pardir, os.pardir))
|
||||
os.chdir(new_home)
|
||||
sys.path.append(new_home)
|
||||
|
||||
from Utils import init_logging, local_path
|
||||
local_path.cached_path = new_home
|
||||
from BaseClasses import MultiWorld, CollectionState, Location
|
||||
from worlds import AutoWorld
|
||||
from worlds.AutoWorld import call_all
|
||||
|
||||
init_logging("Benchmark Runner")
|
||||
logger = logging.getLogger("Benchmark")
|
||||
|
||||
|
||||
class BenchmarkRunner:
|
||||
gen_steps: typing.Tuple[str, ...] = (
|
||||
"generate_early", "create_regions", "create_items", "set_rules", "generate_basic", "pre_fill")
|
||||
rule_iterations: int = 100_000
|
||||
|
||||
if sys.version_info >= (3, 9):
|
||||
@staticmethod
|
||||
def format_times_from_counter(counter: collections.Counter[str], top: int = 5) -> str:
|
||||
return "\n".join(f" {time:.4f} in {name}" for name, time in counter.most_common(top))
|
||||
else:
|
||||
@staticmethod
|
||||
def format_times_from_counter(counter: collections.Counter, top: int = 5) -> str:
|
||||
return "\n".join(f" {time:.4f} in {name}" for name, time in counter.most_common(top))
|
||||
|
||||
def location_test(self, test_location: Location, state: CollectionState, state_name: str) -> float:
|
||||
with TimeIt(f"{test_location.game} {self.rule_iterations} "
|
||||
f"runs of {test_location}.access_rule({state_name})", logger) as t:
|
||||
for _ in range(self.rule_iterations):
|
||||
test_location.access_rule(state)
|
||||
# if time is taken to disentangle complex ref chains,
|
||||
# this time should be attributed to the rule.
|
||||
gc.collect()
|
||||
return t.dif
|
||||
|
||||
def main(self):
|
||||
for game in sorted(AutoWorld.AutoWorldRegister.world_types):
|
||||
summary_data: typing.Dict[str, collections.Counter[str]] = {
|
||||
"empty_state": collections.Counter(),
|
||||
"all_state": collections.Counter(),
|
||||
}
|
||||
try:
|
||||
multiworld = MultiWorld(1)
|
||||
multiworld.game[1] = game
|
||||
multiworld.player_name = {1: "Tester"}
|
||||
multiworld.set_seed(0)
|
||||
multiworld.state = CollectionState(multiworld)
|
||||
args = argparse.Namespace()
|
||||
for name, option in AutoWorld.AutoWorldRegister.world_types[game].options_dataclass.type_hints.items():
|
||||
setattr(args, name, {
|
||||
1: option.from_any(getattr(option, "default"))
|
||||
})
|
||||
multiworld.set_options(args)
|
||||
|
||||
gc.collect()
|
||||
for step in self.gen_steps:
|
||||
with TimeIt(f"{game} step {step}", logger):
|
||||
call_all(multiworld, step)
|
||||
gc.collect()
|
||||
|
||||
locations = sorted(multiworld.get_unfilled_locations())
|
||||
if not locations:
|
||||
continue
|
||||
|
||||
all_state = multiworld.get_all_state(False)
|
||||
for location in locations:
|
||||
time_taken = self.location_test(location, multiworld.state, "empty_state")
|
||||
summary_data["empty_state"][location.name] = time_taken
|
||||
|
||||
time_taken = self.location_test(location, all_state, "all_state")
|
||||
summary_data["all_state"][location.name] = time_taken
|
||||
|
||||
total_empty_state = sum(summary_data["empty_state"].values())
|
||||
total_all_state = sum(summary_data["all_state"].values())
|
||||
|
||||
logger.info(f"{game} took {total_empty_state/len(locations):.4f} "
|
||||
f"seconds per location in empty_state and {total_all_state/len(locations):.4f} "
|
||||
f"in all_state. (all times summed for {self.rule_iterations} runs.)")
|
||||
logger.info(f"Top times in empty_state:\n"
|
||||
f"{self.format_times_from_counter(summary_data['empty_state'])}")
|
||||
logger.info(f"Top times in all_state:\n"
|
||||
f"{self.format_times_from_counter(summary_data['all_state'])}")
|
||||
|
||||
except Exception as e:
|
||||
logger.exception(e)
|
||||
|
||||
runner = BenchmarkRunner()
|
||||
runner.main()
|
||||
@@ -1,6 +1,5 @@
|
||||
import unittest
|
||||
|
||||
from worlds.AutoWorld import AutoWorldRegister, call_all
|
||||
from worlds.AutoWorld import AutoWorldRegister
|
||||
from . import setup_solo_multiworld
|
||||
|
||||
|
||||
@@ -54,7 +53,7 @@ class TestBase(unittest.TestCase):
|
||||
f"{game_name} Item count MUST meet or exceed the number of locations",
|
||||
)
|
||||
|
||||
def test_items_in_datapackage(self):
|
||||
def testItemsInDatapackage(self):
|
||||
"""Test that any created items in the itempool are in the datapackage"""
|
||||
for game_name, world_type in AutoWorldRegister.world_types.items():
|
||||
with self.subTest("Game", game=game_name):
|
||||
@@ -70,20 +69,3 @@ class TestBase(unittest.TestCase):
|
||||
with self.subTest("Name should be valid", game=game_name, item=name):
|
||||
self.assertIn(name, valid_names,
|
||||
"All item descriptions must match defined item names")
|
||||
|
||||
def test_itempool_not_modified(self):
|
||||
"""Test that worlds don't modify the itempool after `create_items`"""
|
||||
gen_steps = ("generate_early", "create_regions", "create_items")
|
||||
additional_steps = ("set_rules", "generate_basic", "pre_fill")
|
||||
excluded_games = ("Links Awakening DX", "Ocarina of Time", "SMZ3")
|
||||
worlds_to_test = {game: world
|
||||
for game, world in AutoWorldRegister.world_types.items() if game not in excluded_games}
|
||||
for game_name, world_type in worlds_to_test.items():
|
||||
with self.subTest("Game", game=game_name):
|
||||
multiworld = setup_solo_multiworld(world_type, gen_steps)
|
||||
created_items = multiworld.itempool.copy()
|
||||
for step in additional_steps:
|
||||
with self.subTest("step", step=step):
|
||||
call_all(multiworld, step)
|
||||
self.assertEqual(created_items, multiworld.itempool,
|
||||
f"{game_name} modified the itempool during {step}")
|
||||
|
||||
@@ -10,10 +10,3 @@ class TestOptions(unittest.TestCase):
|
||||
for option_key, option in world_type.options_dataclass.type_hints.items():
|
||||
with self.subTest(game=gamename, option=option_key):
|
||||
self.assertTrue(option.__doc__)
|
||||
|
||||
def test_options_are_not_set_by_world(self):
|
||||
"""Test that options attribute is not already set"""
|
||||
for gamename, world_type in AutoWorldRegister.world_types.items():
|
||||
with self.subTest(game=gamename):
|
||||
self.assertFalse(hasattr(world_type, "options"),
|
||||
f"Unexpected assignment to {world_type.__name__}.options!")
|
||||
|
||||
@@ -37,7 +37,7 @@ class TestBase(unittest.TestCase):
|
||||
unreachable_regions = self.default_settings_unreachable_regions.get(game_name, set())
|
||||
with self.subTest("Game", game=game_name):
|
||||
world = setup_solo_multiworld(world_type)
|
||||
excluded = world.worlds[1].options.exclude_locations.value
|
||||
excluded = world.exclude_locations[1].value
|
||||
state = world.get_all_state(False)
|
||||
for location in world.get_locations():
|
||||
if location.name not in excluded:
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import io
|
||||
import unittest
|
||||
import json
|
||||
import yaml
|
||||
|
||||
|
||||
class TestDocs(unittest.TestCase):
|
||||
@@ -25,7 +23,7 @@ class TestDocs(unittest.TestCase):
|
||||
response = self.client.post("/api/generate")
|
||||
self.assertIn("No options found. Expected file attachment or json weights.", response.text)
|
||||
|
||||
def test_generation_queued_weights(self):
|
||||
def test_generation_queued(self):
|
||||
options = {
|
||||
"Tester1":
|
||||
{
|
||||
@@ -42,19 +40,3 @@ class TestDocs(unittest.TestCase):
|
||||
json_data = response.get_json()
|
||||
self.assertTrue(json_data["text"].startswith("Generation of seed "))
|
||||
self.assertTrue(json_data["text"].endswith(" started successfully."))
|
||||
|
||||
def test_generation_queued_file(self):
|
||||
options = {
|
||||
"game": "Archipelago",
|
||||
"name": "Tester",
|
||||
"Archipelago": {}
|
||||
}
|
||||
response = self.client.post(
|
||||
"/api/generate",
|
||||
data={
|
||||
'file': (io.BytesIO(yaml.dump(options, encoding="utf-8")), "test.yaml")
|
||||
},
|
||||
)
|
||||
json_data = response.get_json()
|
||||
self.assertTrue(json_data["text"].startswith("Generation of seed "))
|
||||
self.assertTrue(json_data["text"].endswith(" started successfully."))
|
||||
|
||||
@@ -1,12 +1,24 @@
|
||||
from .texture import FillType_Drawable, FillType_Vec, Texture
|
||||
""" FillType_* is not a real kivy type - just something to fill unknown typing. """
|
||||
|
||||
from typing import Sequence
|
||||
|
||||
FillType_Vec = Sequence[int]
|
||||
|
||||
|
||||
class FillType_Drawable:
|
||||
def __init__(self, *, pos: FillType_Vec = ..., size: FillType_Vec = ...) -> None: ...
|
||||
|
||||
|
||||
class FillType_Texture(FillType_Drawable):
|
||||
pass
|
||||
|
||||
|
||||
class FillType_Shape(FillType_Drawable):
|
||||
texture: Texture
|
||||
texture: FillType_Texture
|
||||
|
||||
def __init__(self,
|
||||
*,
|
||||
texture: Texture = ...,
|
||||
texture: FillType_Texture = ...,
|
||||
pos: FillType_Vec = ...,
|
||||
size: FillType_Vec = ...) -> None: ...
|
||||
|
||||
@@ -23,6 +35,6 @@ class Rectangle(FillType_Shape):
|
||||
def __init__(self,
|
||||
*,
|
||||
source: str = ...,
|
||||
texture: Texture = ...,
|
||||
texture: FillType_Texture = ...,
|
||||
pos: FillType_Vec = ...,
|
||||
size: FillType_Vec = ...) -> None: ...
|
||||
@@ -1,13 +0,0 @@
|
||||
""" FillType_* is not a real kivy type - just something to fill unknown typing. """
|
||||
|
||||
from typing import Sequence
|
||||
|
||||
FillType_Vec = Sequence[int]
|
||||
|
||||
|
||||
class FillType_Drawable:
|
||||
def __init__(self, *, pos: FillType_Vec = ..., size: FillType_Vec = ...) -> None: ...
|
||||
|
||||
|
||||
class Texture:
|
||||
pass
|
||||
@@ -1,9 +0,0 @@
|
||||
import io
|
||||
|
||||
from kivy.graphics.texture import Texture
|
||||
|
||||
|
||||
class CoreImage:
|
||||
texture: Texture
|
||||
|
||||
def __init__(self, data: io.BytesIO, ext: str) -> None: ...
|
||||
@@ -39,8 +39,7 @@ class AutoWorldRegister(type):
|
||||
|
||||
def __new__(mcs, name: str, bases: Tuple[type, ...], dct: Dict[str, Any]) -> AutoWorldRegister:
|
||||
if "web" in dct:
|
||||
assert isinstance(dct["web"], WebWorld) or isinstance(dct["web"], staticproperty), \
|
||||
"WebWorld has to be instantiated."
|
||||
assert isinstance(dct["web"], WebWorld), "WebWorld has to be instantiated."
|
||||
# filter out any events
|
||||
dct["item_name_to_id"] = {name: id for name, id in dct["item_name_to_id"].items() if id}
|
||||
dct["location_name_to_id"] = {name: id for name, id in dct["location_name_to_id"].items() if id}
|
||||
@@ -80,8 +79,8 @@ class AutoWorldRegister(type):
|
||||
if "options_dataclass" not in dct and "option_definitions" in dct:
|
||||
# TODO - switch to deprecate after a version
|
||||
if __debug__:
|
||||
logging.warning(f"{name} Assigned options through option_definitions which is now deprecated. "
|
||||
"Please use options_dataclass instead.")
|
||||
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,))
|
||||
|
||||
@@ -329,7 +328,7 @@ class World(metaclass=AutoWorldRegister):
|
||||
|
||||
def create_items(self) -> None:
|
||||
"""
|
||||
Method for creating and submitting items to the itempool. Items and Regions must *not* be created and submitted
|
||||
Method for creating and submitting items to the itempool. Items and Regions should *not* be created and submitted
|
||||
to the MultiWorld after this step. If items need to be placed during pre_fill use `get_prefill_items`.
|
||||
"""
|
||||
pass
|
||||
@@ -485,11 +484,6 @@ class LogicMixin(metaclass=AutoLogicRegister):
|
||||
pass
|
||||
|
||||
|
||||
class staticproperty(staticmethod):
|
||||
def __get__(self, *args):
|
||||
return self.__func__()
|
||||
|
||||
|
||||
def data_package_checksum(data: "GamesPackage") -> str:
|
||||
"""Calculates the data package checksum for a game from a dict"""
|
||||
assert "checksum" not in data, "Checksum already in data"
|
||||
|
||||
@@ -2,7 +2,9 @@ from __future__ import annotations
|
||||
|
||||
from typing import Dict
|
||||
|
||||
from Options import Choice, Option, DefaultOnToggle, DeathLink, Range, Toggle
|
||||
from Options import Choice, Option, DefaultOnToggle, DeathLink, Range, Toggle, PerGameCommonOptions
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
class FreeincarnateMax(Range):
|
||||
@@ -224,21 +226,20 @@ class StartCastle(Choice):
|
||||
default = option_yellow
|
||||
|
||||
|
||||
adventure_option_definitions: Dict[str, type(Option)] = {
|
||||
"dragon_slay_check": DragonSlayCheck,
|
||||
"death_link": DeathLink,
|
||||
"bat_logic": BatLogic,
|
||||
"freeincarnate_max": FreeincarnateMax,
|
||||
"dragon_rando_type": DragonRandoType,
|
||||
"connector_multi_slot": ConnectorMultiSlot,
|
||||
"yorgle_speed": YorgleStartingSpeed,
|
||||
"yorgle_min_speed": YorgleMinimumSpeed,
|
||||
"grundle_speed": GrundleStartingSpeed,
|
||||
"grundle_min_speed": GrundleMinimumSpeed,
|
||||
"rhindle_speed": RhindleStartingSpeed,
|
||||
"rhindle_min_speed": RhindleMinimumSpeed,
|
||||
"difficulty_switch_a": DifficultySwitchA,
|
||||
"difficulty_switch_b": DifficultySwitchB,
|
||||
"start_castle": StartCastle,
|
||||
|
||||
}
|
||||
@dataclass
|
||||
class AdventureOptions(PerGameCommonOptions):
|
||||
dragon_slay_check: DragonSlayCheck
|
||||
death_link: DeathLink
|
||||
bat_logic: BatLogic
|
||||
freeincarnate_max: FreeincarnateMax
|
||||
dragon_rando_type: DragonRandoType
|
||||
connector_multi_slot: ConnectorMultiSlot
|
||||
yorgle_speed: YorgleStartingSpeed
|
||||
yorgle_min_speed: YorgleMinimumSpeed
|
||||
grundle_speed: GrundleStartingSpeed
|
||||
grundle_min_speed: GrundleMinimumSpeed
|
||||
rhindle_speed: RhindleStartingSpeed
|
||||
rhindle_min_speed: RhindleMinimumSpeed
|
||||
difficulty_switch_a: DifficultySwitchA
|
||||
difficulty_switch_b: DifficultySwitchB
|
||||
start_castle: StartCastle
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from BaseClasses import MultiWorld, Region, Entrance, LocationProgressType
|
||||
from .Locations import location_table, LocationData, AdventureLocation, dragon_room_to_region
|
||||
from Options import PerGameCommonOptions
|
||||
|
||||
|
||||
def connect(world: MultiWorld, player: int, source: str, target: str, rule: callable = lambda state: True,
|
||||
@@ -24,7 +25,7 @@ def connect(world: MultiWorld, player: int, source: str, target: str, rule: call
|
||||
connect(world, player, target, source, rule, True)
|
||||
|
||||
|
||||
def create_regions(multiworld: MultiWorld, player: int, dragon_rooms: []) -> None:
|
||||
def create_regions(options: PerGameCommonOptions, multiworld: MultiWorld, player: int, dragon_rooms: []) -> None:
|
||||
for name, locdata in location_table.items():
|
||||
locdata.get_position(multiworld.random)
|
||||
|
||||
@@ -76,7 +77,7 @@ def create_regions(multiworld: MultiWorld, player: int, dragon_rooms: []) -> Non
|
||||
credits_room_far_side.exits.append(Entrance(player, "CreditsFromFarSide", credits_room_far_side))
|
||||
multiworld.regions.append(credits_room_far_side)
|
||||
|
||||
dragon_slay_check = multiworld.dragon_slay_check[player].value
|
||||
dragon_slay_check = options.dragon_slay_check.value
|
||||
priority_locations = determine_priority_locations(multiworld, dragon_slay_check)
|
||||
|
||||
for name, location_data in location_table.items():
|
||||
|
||||
@@ -6,7 +6,8 @@ from BaseClasses import LocationProgressType
|
||||
|
||||
def set_rules(self) -> None:
|
||||
world = self.multiworld
|
||||
use_bat_logic = world.bat_logic[self.player].value == BatLogic.option_use_logic
|
||||
options = self.options
|
||||
use_bat_logic = options.bat_logic.value == BatLogic.option_use_logic
|
||||
|
||||
set_rule(world.get_entrance("YellowCastlePort", self.player),
|
||||
lambda state: state.has("Yellow Key", self.player))
|
||||
@@ -28,7 +29,7 @@ def set_rules(self) -> None:
|
||||
lambda state: state.has("Bridge", self.player) or
|
||||
state.has("Magnet", self.player))
|
||||
|
||||
dragon_slay_check = world.dragon_slay_check[self.player].value
|
||||
dragon_slay_check = options.dragon_slay_check.value
|
||||
if dragon_slay_check:
|
||||
if self.difficulty_switch_b == DifficultySwitchB.option_hard_with_unlock_item:
|
||||
set_rule(world.get_location("Slay Yorgle", self.player),
|
||||
|
||||
@@ -15,7 +15,7 @@ from Options import AssembleOptions
|
||||
from worlds.AutoWorld import WebWorld, World
|
||||
from Fill import fill_restrictive
|
||||
from worlds.generic.Rules import add_rule, set_rule
|
||||
from .Options import adventure_option_definitions, DragonRandoType, DifficultySwitchA, DifficultySwitchB
|
||||
from .Options import AdventureOptions, DragonRandoType, DifficultySwitchA, DifficultySwitchB
|
||||
from .Rom import get_base_rom_bytes, get_base_rom_path, AdventureDeltaPatch, apply_basepatch, \
|
||||
AdventureAutoCollectLocation
|
||||
from .Items import item_table, ItemData, nothing_item_id, event_table, AdventureItem, standard_item_max
|
||||
@@ -109,7 +109,8 @@ class AdventureWorld(World):
|
||||
game: ClassVar[str] = "Adventure"
|
||||
web: ClassVar[WebWorld] = AdventureWeb()
|
||||
|
||||
option_definitions: ClassVar[Dict[str, AssembleOptions]] = adventure_option_definitions
|
||||
options = AdventureOptions
|
||||
options_dataclass = AdventureOptions
|
||||
settings: ClassVar[AdventureSettings]
|
||||
item_name_to_id: ClassVar[Dict[str, int]] = {name: data.id for name, data in item_table.items()}
|
||||
location_name_to_id: ClassVar[Dict[str, int]] = {name: data.location_id for name, data in location_table.items()}
|
||||
@@ -150,18 +151,18 @@ class AdventureWorld(World):
|
||||
bytearray(f"ADVENTURE{__version__.replace('.', '')[:3]}_{self.player}_{self.multiworld.seed}", "utf8")[:21]
|
||||
self.rom_name.extend([0] * (21 - len(self.rom_name)))
|
||||
|
||||
self.dragon_rando_type = self.multiworld.dragon_rando_type[self.player].value
|
||||
self.dragon_slay_check = self.multiworld.dragon_slay_check[self.player].value
|
||||
self.connector_multi_slot = self.multiworld.connector_multi_slot[self.player].value
|
||||
self.yorgle_speed = self.multiworld.yorgle_speed[self.player].value
|
||||
self.yorgle_min_speed = self.multiworld.yorgle_min_speed[self.player].value
|
||||
self.grundle_speed = self.multiworld.grundle_speed[self.player].value
|
||||
self.grundle_min_speed = self.multiworld.grundle_min_speed[self.player].value
|
||||
self.rhindle_speed = self.multiworld.rhindle_speed[self.player].value
|
||||
self.rhindle_min_speed = self.multiworld.rhindle_min_speed[self.player].value
|
||||
self.difficulty_switch_a = self.multiworld.difficulty_switch_a[self.player].value
|
||||
self.difficulty_switch_b = self.multiworld.difficulty_switch_b[self.player].value
|
||||
self.start_castle = self.multiworld.start_castle[self.player].value
|
||||
self.dragon_rando_type = self.options.dragon_rando_type.value
|
||||
self.dragon_slay_check = self.options.dragon_slay_check.value
|
||||
self.connector_multi_slot = self.options.connector_multi_slot.value
|
||||
self.yorgle_speed = self.options.yorgle_speed.value
|
||||
self.yorgle_min_speed = self.options.yorgle_min_speed.value
|
||||
self.grundle_speed = self.options.grundle_speed.value
|
||||
self.grundle_min_speed = self.options.grundle_min_speed.value
|
||||
self.rhindle_speed = self.options.rhindle_speed.value
|
||||
self.rhindle_min_speed = self.options.rhindle_min_speed.value
|
||||
self.difficulty_switch_a = self.options.difficulty_switch_a.value
|
||||
self.difficulty_switch_b = self.options.difficulty_switch_b.value
|
||||
self.start_castle = self.options.start_castle.value
|
||||
self.created_items = 0
|
||||
|
||||
if self.dragon_slay_check == 0:
|
||||
@@ -228,7 +229,7 @@ class AdventureWorld(World):
|
||||
extra_filler_count = num_locations - self.created_items
|
||||
|
||||
# traps would probably go here, if enabled
|
||||
freeincarnate_max = self.multiworld.freeincarnate_max[self.player].value
|
||||
freeincarnate_max = self.options.freeincarnate_max.value
|
||||
actual_freeincarnates = min(extra_filler_count, freeincarnate_max)
|
||||
self.multiworld.itempool += [self.create_item("Freeincarnate") for _ in range(actual_freeincarnates)]
|
||||
self.created_items += actual_freeincarnates
|
||||
@@ -248,7 +249,7 @@ class AdventureWorld(World):
|
||||
self.created_items += 1
|
||||
|
||||
def create_regions(self) -> None:
|
||||
create_regions(self.multiworld, self.player, self.dragon_rooms)
|
||||
create_regions(self.options, self.multiworld, self.player, self.dragon_rooms)
|
||||
|
||||
set_rules = set_rules
|
||||
|
||||
@@ -355,7 +356,7 @@ class AdventureWorld(World):
|
||||
auto_collect_locations: [AdventureAutoCollectLocation] = []
|
||||
local_item_to_location: {int, int} = {}
|
||||
bat_no_touch_locs: [LocationData] = []
|
||||
bat_logic: int = self.multiworld.bat_logic[self.player].value
|
||||
bat_logic: int = self.options.bat_logic.value
|
||||
try:
|
||||
rom_deltas: { int, int } = {}
|
||||
self.place_dragons(rom_deltas)
|
||||
@@ -411,7 +412,7 @@ class AdventureWorld(World):
|
||||
item_position_data_start = get_item_position_data_start(unplaced_item.table_index)
|
||||
rom_deltas[item_position_data_start] = 0xff
|
||||
|
||||
if self.multiworld.connector_multi_slot[self.player].value:
|
||||
if self.options.connector_multi_slot.value:
|
||||
rom_deltas[connector_port_offset] = (self.player & 0xff)
|
||||
else:
|
||||
rom_deltas[connector_port_offset] = 0
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -26,13 +26,6 @@ class ALttPLocation(Location):
|
||||
self.player_address = player_address
|
||||
self._hint_text = hint_text
|
||||
|
||||
@property
|
||||
def hint_text(self) -> str:
|
||||
hint_text = getattr(self, "_hint_text", None)
|
||||
if hint_text:
|
||||
return hint_text
|
||||
return "at " + self.name.replace("_", " ").replace("-", " ")
|
||||
|
||||
|
||||
class ALttPItem(Item):
|
||||
game: str = "A Link to the Past"
|
||||
|
||||
@@ -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...**
|
||||
|
||||
@@ -7,25 +7,16 @@ from ..AutoWorld import WebWorld, World
|
||||
class Bk_SudokuWebWorld(WebWorld):
|
||||
options_page = "games/Sudoku/info/en"
|
||||
theme = 'partyTime'
|
||||
|
||||
setup_en = Tutorial(
|
||||
tutorial_name='Setup Guide',
|
||||
description='A guide to playing BK Sudoku',
|
||||
language='English',
|
||||
file_name='setup_en.md',
|
||||
link='setup/en',
|
||||
authors=['Jarno']
|
||||
)
|
||||
setup_de = Tutorial(
|
||||
tutorial_name='Setup Anleitung',
|
||||
description='Eine Anleitung um BK-Sudoku zu spielen',
|
||||
language='Deutsch',
|
||||
file_name='setup_de.md',
|
||||
link='setup/de',
|
||||
authors=['Held_der_Zeit']
|
||||
)
|
||||
|
||||
tutorials = [setup_en, setup_de]
|
||||
tutorials = [
|
||||
Tutorial(
|
||||
tutorial_name='Setup Guide',
|
||||
description='A guide to playing BK Sudoku',
|
||||
language='English',
|
||||
file_name='setup_en.md',
|
||||
link='setup/en',
|
||||
authors=['Jarno']
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
class Bk_SudokuWorld(World):
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
# BK-Sudoku
|
||||
|
||||
## Was ist das für ein Spiel?
|
||||
|
||||
BK-Sudoku ist kein typisches Archipelago-Spiel; stattdessen ist es ein gewöhnlicher Sudoku-Client der sich zu jeder
|
||||
beliebigen Multiworld verbinden kann. Einmal verbunden kannst du ein 9x9 Sudoku spielen um einen zufälligen Hinweis
|
||||
für dein Spiel zu erhalten. Es ist zwar langsam, aber es gibt dir etwas zu tun, solltest du mal nicht in der Lage sein
|
||||
weitere „Checks” zu erreichen.
|
||||
(Wer mag kann auch einfach so Sudoku spielen. Man muss nicht mit einer Multiworld verbunden sein, um ein Sudoku zu
|
||||
spielen/generieren.)
|
||||
|
||||
## Wie werden Hinweise freigeschalten?
|
||||
|
||||
Nach dem Lösen eines Sudokus wird für den verbundenen Slot ein zufällig ausgewählter Hinweis freigegeben, für einen
|
||||
Gegenstand der noch nicht gefunden wurde.
|
||||
|
||||
## Wo ist die Seite für die Einstellungen?
|
||||
|
||||
Es gibt keine Seite für die Einstellungen. Dieses Spiel kann nicht in deinen YAML-Dateien benutzt werden. Stattdessen
|
||||
kann sich der Client mit einem beliebigen Slot einer Multiworld verbinden. In dem Client selbst kann aber der
|
||||
Schwierigkeitsgrad des Sudoku ausgewählt werden.
|
||||
@@ -1,27 +0,0 @@
|
||||
# BK-Sudoku Setup Anleitung
|
||||
|
||||
## Benötigte Software
|
||||
- [Bk-Sudoku](https://github.com/Jarno458/sudoku)
|
||||
- Windows 8 oder höher
|
||||
|
||||
## Generelles Konzept
|
||||
|
||||
Dies ist ein Client, der sich mit jedem beliebigen Slot einer Multiworld verbinden kann. Er lässt dich ein (9x9) Sudoku
|
||||
spielen, um zufällige Hinweise für den verbundenen Slot freizuschalten.
|
||||
|
||||
Aufgrund des Fakts, dass der Sudoku-Client sich zu jedem beliebigen Slot verbinden kann, ist es daher nicht notwendig
|
||||
eine YAML für dieses Spiel zu generieren, da es keinen neuen Slot zur Multiworld-Session hinzufügt.
|
||||
|
||||
## Installationsprozess
|
||||
|
||||
Gehe zu der aktuellsten (latest) Veröffentlichung der [BK-Sudoku Releases](https://github.com/Jarno458/sudoku/releases).
|
||||
Downloade und extrahiere/entpacke die `Bk_Sudoku.zip`-Datei.
|
||||
|
||||
## Verbinden mit einer Multiworld
|
||||
|
||||
1. Starte `Bk_Sudoku.exe`
|
||||
2. Trage den Namen des Slots ein, mit dem du dich verbinden möchtest
|
||||
3. Trage die Server-URL und den Port ein
|
||||
4. Drücke auf Verbinden (connect)
|
||||
5. Wähle deinen Schwierigkeitsgrad
|
||||
6. Versuche das Sudoku zu Lösen
|
||||
@@ -1,4 +1,5 @@
|
||||
from Options import Choice, Toggle, DefaultOnToggle, DeathLink, StartInventoryPool
|
||||
from Options import Choice, Toggle, DefaultOnToggle, DeathLink, StartInventoryPool, PerGameCommonOptions
|
||||
from dataclasses import dataclass
|
||||
import random
|
||||
|
||||
|
||||
@@ -163,26 +164,26 @@ class BlasphemousDeathLink(DeathLink):
|
||||
Note that Guilt Fragments will not appear when killed by Death Link."""
|
||||
|
||||
|
||||
blasphemous_options = {
|
||||
"prie_dieu_warp": PrieDieuWarp,
|
||||
"skip_cutscenes": SkipCutscenes,
|
||||
"corpse_hints": CorpseHints,
|
||||
"difficulty": Difficulty,
|
||||
"penitence": Penitence,
|
||||
"starting_location": StartingLocation,
|
||||
"ending": Ending,
|
||||
"skip_long_quests": SkipLongQuests,
|
||||
"thorn_shuffle" : ThornShuffle,
|
||||
"dash_shuffle": DashShuffle,
|
||||
"wall_climb_shuffle": WallClimbShuffle,
|
||||
"reliquary_shuffle": ReliquaryShuffle,
|
||||
"boots_of_pleading": CustomItem1,
|
||||
"purified_hand": CustomItem2,
|
||||
"start_wheel": StartWheel,
|
||||
"skill_randomizer": SkillRando,
|
||||
"enemy_randomizer": EnemyRando,
|
||||
"enemy_groups": EnemyGroups,
|
||||
"enemy_scaling": EnemyScaling,
|
||||
"death_link": BlasphemousDeathLink,
|
||||
"start_inventory": StartInventoryPool
|
||||
}
|
||||
@dataclass
|
||||
class BlasphemousOptions(PerGameCommonOptions):
|
||||
prie_dieu_warp: PrieDieuWarp
|
||||
skip_cutscenes: SkipCutscenes
|
||||
corpse_hints: CorpseHints
|
||||
difficulty: Difficulty
|
||||
penitence: Penitence
|
||||
starting_location: StartingLocation
|
||||
ending: Ending
|
||||
skip_long_quests: SkipLongQuests
|
||||
thorn_shuffle : ThornShuffle
|
||||
dash_shuffle: DashShuffle
|
||||
wall_climb_shuffle: WallClimbShuffle
|
||||
reliquary_shuffle: ReliquaryShuffle
|
||||
boots_of_pleading: CustomItem1
|
||||
purified_hand: CustomItem2
|
||||
start_wheel: StartWheel
|
||||
skill_randomizer: SkillRando
|
||||
enemy_randomizer: EnemyRando
|
||||
enemy_groups: EnemyGroups
|
||||
enemy_scaling: EnemyScaling
|
||||
death_link: BlasphemousDeathLink
|
||||
start_inventory: StartInventoryPool
|
||||
@@ -497,8 +497,9 @@ def chalice_rooms(state: CollectionState, player: int, number: int) -> bool:
|
||||
def rules(blasphemousworld):
|
||||
world = blasphemousworld.multiworld
|
||||
player = blasphemousworld.player
|
||||
logic = world.difficulty[player].value
|
||||
enemy = world.enemy_randomizer[player].value
|
||||
options = blasphemousworld.options
|
||||
logic = options.difficulty.value
|
||||
enemy = options.enemy_randomizer.value
|
||||
|
||||
|
||||
# D01Z01S01 (The Holy Line)
|
||||
@@ -2488,7 +2489,7 @@ def rules(blasphemousworld):
|
||||
|
||||
# D04Z02S01 (Mother of Mothers)
|
||||
# Items
|
||||
if world.purified_hand[player]:
|
||||
if options.purified_hand:
|
||||
set_rule(world.get_location("MoM: Western room ledge", player),
|
||||
lambda state: (
|
||||
state.has("D04Z02S01[N]", player)
|
||||
@@ -4093,7 +4094,7 @@ def rules(blasphemousworld):
|
||||
|
||||
# D17Z01S04 (Brotherhood of the Silent Sorrow)
|
||||
# Items
|
||||
if world.boots_of_pleading[player]:
|
||||
if options.boots_of_pleading:
|
||||
set_rule(world.get_location("BotSS: 2nd meeting with Redento", player),
|
||||
lambda state: redento(state, blasphemousworld, player, 2))
|
||||
# Doors
|
||||
|
||||
@@ -7,7 +7,7 @@ from .Locations import location_table
|
||||
from .Rooms import room_table, door_table
|
||||
from .Rules import rules
|
||||
from worlds.generic.Rules import set_rule, add_rule
|
||||
from .Options import blasphemous_options
|
||||
from .Options import BlasphemousOptions
|
||||
from .Vanilla import unrandomized_dict, junk_locations, thorn_set, skill_dict
|
||||
|
||||
|
||||
@@ -39,7 +39,8 @@ class BlasphemousWorld(World):
|
||||
location_name_to_game_id = {loc["name"]: loc["game_id"] for loc in location_table}
|
||||
|
||||
item_name_groups = group_table
|
||||
option_definitions = blasphemous_options
|
||||
options = BlasphemousOptions
|
||||
options_dataclass = BlasphemousOptions
|
||||
|
||||
required_client_version = (0, 4, 2)
|
||||
|
||||
@@ -73,60 +74,61 @@ class BlasphemousWorld(World):
|
||||
|
||||
|
||||
def generate_early(self):
|
||||
options = self.options
|
||||
world = self.multiworld
|
||||
player = self.player
|
||||
|
||||
if not world.starting_location[player].randomized:
|
||||
if world.starting_location[player].value == 6 and world.difficulty[player].value < 2:
|
||||
raise Exception(f"[Blasphemous - '{world.get_player_name(player)}'] {world.starting_location[player]}"
|
||||
if not options.starting_location.randomized:
|
||||
if options.starting_location.value == 6 and options.difficulty.value < 2:
|
||||
raise Exception(f"[Blasphemous - '{world.get_player_name(player)}'] {options.starting_location}"
|
||||
" cannot be chosen if Difficulty is lower than Hard.")
|
||||
|
||||
if (world.starting_location[player].value == 0 or world.starting_location[player].value == 6) \
|
||||
and world.dash_shuffle[player]:
|
||||
raise Exception(f"[Blasphemous - '{world.get_player_name(player)}'] {world.starting_location[player]}"
|
||||
if (options.starting_location.value == 0 or options.starting_location.value == 6) \
|
||||
and options.dash_shuffle:
|
||||
raise Exception(f"[Blasphemous - '{world.get_player_name(player)}'] {options.starting_location}"
|
||||
" cannot be chosen if Shuffle Dash is enabled.")
|
||||
|
||||
if world.starting_location[player].value == 3 and world.wall_climb_shuffle[player]:
|
||||
raise Exception(f"[Blasphemous - '{world.get_player_name(player)}'] {world.starting_location[player]}"
|
||||
if options.starting_location.value == 3 and options.wall_climb_shuffle:
|
||||
raise Exception(f"[Blasphemous - '{world.get_player_name(player)}'] {options.starting_location}"
|
||||
" cannot be chosen if Shuffle Wall Climb is enabled.")
|
||||
else:
|
||||
locations: List[int] = [ 0, 1, 2, 3, 4, 5, 6 ]
|
||||
invalid: bool = False
|
||||
|
||||
if world.difficulty[player].value < 2:
|
||||
if options.difficulty.value < 2:
|
||||
locations.remove(6)
|
||||
|
||||
if world.dash_shuffle[player]:
|
||||
if options.dash_shuffle:
|
||||
locations.remove(0)
|
||||
if 6 in locations:
|
||||
locations.remove(6)
|
||||
|
||||
if world.wall_climb_shuffle[player]:
|
||||
if options.wall_climb_shuffle:
|
||||
locations.remove(3)
|
||||
|
||||
if world.starting_location[player].value == 6 and world.difficulty[player].value < 2:
|
||||
if options.starting_location.value == 6 and options.difficulty.value < 2:
|
||||
invalid = True
|
||||
|
||||
if (world.starting_location[player].value == 0 or world.starting_location[player].value == 6) \
|
||||
and world.dash_shuffle[player]:
|
||||
if (options.starting_location.value == 0 or options.starting_location.value == 6) \
|
||||
and options.dash_shuffle:
|
||||
invalid = True
|
||||
|
||||
if world.starting_location[player].value == 3 and world.wall_climb_shuffle[player]:
|
||||
if options.starting_location.value == 3 and options.wall_climb_shuffle:
|
||||
invalid = True
|
||||
|
||||
if invalid:
|
||||
world.starting_location[player].value = world.random.choice(locations)
|
||||
options.starting_location.value = world.random.choice(locations)
|
||||
|
||||
|
||||
if not world.dash_shuffle[player]:
|
||||
if not options.dash_shuffle:
|
||||
world.push_precollected(self.create_item("Dash Ability"))
|
||||
|
||||
if not world.wall_climb_shuffle[player]:
|
||||
if not options.wall_climb_shuffle:
|
||||
world.push_precollected(self.create_item("Wall Climb Ability"))
|
||||
|
||||
if world.skip_long_quests[player]:
|
||||
if options.skip_long_quests:
|
||||
for loc in junk_locations:
|
||||
world.exclude_locations[player].value.add(loc)
|
||||
options.exclude_locations.value.add(loc)
|
||||
|
||||
start_rooms: Dict[int, str] = {
|
||||
0: "D17Z01S01",
|
||||
@@ -138,12 +140,12 @@ class BlasphemousWorld(World):
|
||||
6: "D20Z02S09"
|
||||
}
|
||||
|
||||
self.start_room = start_rooms[world.starting_location[player].value]
|
||||
self.start_room = start_rooms[options.starting_location.value]
|
||||
|
||||
|
||||
def create_items(self):
|
||||
options = self.options
|
||||
world = self.multiworld
|
||||
player = self.player
|
||||
|
||||
removed: int = 0
|
||||
to_remove: List[str] = [
|
||||
@@ -157,46 +159,46 @@ class BlasphemousWorld(World):
|
||||
skipped_items = []
|
||||
junk: int = 0
|
||||
|
||||
for item, count in world.start_inventory[player].value.items():
|
||||
for item, count in options.start_inventory.value.items():
|
||||
for _ in range(count):
|
||||
skipped_items.append(item)
|
||||
junk += 1
|
||||
|
||||
skipped_items.extend(unrandomized_dict.values())
|
||||
|
||||
if world.thorn_shuffle[player] == 2:
|
||||
if options.thorn_shuffle == 2:
|
||||
for i in range(8):
|
||||
skipped_items.append("Thorn Upgrade")
|
||||
|
||||
if world.dash_shuffle[player]:
|
||||
if options.dash_shuffle:
|
||||
skipped_items.append(to_remove[removed])
|
||||
removed += 1
|
||||
elif not world.dash_shuffle[player]:
|
||||
elif not options.dash_shuffle:
|
||||
skipped_items.append("Dash Ability")
|
||||
|
||||
if world.wall_climb_shuffle[player]:
|
||||
if options.wall_climb_shuffle:
|
||||
skipped_items.append(to_remove[removed])
|
||||
removed += 1
|
||||
elif not world.wall_climb_shuffle[player]:
|
||||
elif not options.wall_climb_shuffle:
|
||||
skipped_items.append("Wall Climb Ability")
|
||||
|
||||
if not world.reliquary_shuffle[player]:
|
||||
if not options.reliquary_shuffle:
|
||||
skipped_items.extend(reliquary_set)
|
||||
elif world.reliquary_shuffle[player]:
|
||||
elif options.reliquary_shuffle:
|
||||
for i in range(3):
|
||||
skipped_items.append(to_remove[removed])
|
||||
removed += 1
|
||||
|
||||
if not world.boots_of_pleading[player]:
|
||||
if not options.boots_of_pleading:
|
||||
skipped_items.append("Boots of Pleading")
|
||||
|
||||
if not world.purified_hand[player]:
|
||||
if not options.purified_hand:
|
||||
skipped_items.append("Purified Hand of the Nun")
|
||||
|
||||
if world.start_wheel[player]:
|
||||
if options.start_wheel:
|
||||
skipped_items.append("The Young Mason's Wheel")
|
||||
|
||||
if not world.skill_randomizer[player]:
|
||||
if not options.skill_randomizer:
|
||||
skipped_items.extend(skill_dict.values())
|
||||
|
||||
counter = Counter(skipped_items)
|
||||
@@ -219,23 +221,24 @@ class BlasphemousWorld(World):
|
||||
|
||||
|
||||
def pre_fill(self):
|
||||
options = self.options
|
||||
world = self.multiworld
|
||||
player = self.player
|
||||
|
||||
self.place_items_from_dict(unrandomized_dict)
|
||||
|
||||
if world.thorn_shuffle[player] == 2:
|
||||
if options.thorn_shuffle == 2:
|
||||
self.place_items_from_set(thorn_set, "Thorn Upgrade")
|
||||
|
||||
if world.start_wheel[player]:
|
||||
if options.start_wheel:
|
||||
world.get_location("Beginning gift", player)\
|
||||
.place_locked_item(self.create_item("The Young Mason's Wheel"))
|
||||
|
||||
if not world.skill_randomizer[player]:
|
||||
if not options.skill_randomizer:
|
||||
self.place_items_from_dict(skill_dict)
|
||||
|
||||
if world.thorn_shuffle[player] == 1:
|
||||
world.local_items[player].value.add("Thorn Upgrade")
|
||||
if options.thorn_shuffle == 1:
|
||||
options.local_items.value.add("Thorn Upgrade")
|
||||
|
||||
|
||||
def place_items_from_set(self, location_set: Set[str], name: str):
|
||||
@@ -251,6 +254,7 @@ class BlasphemousWorld(World):
|
||||
|
||||
|
||||
def create_regions(self) -> None:
|
||||
options = self.options
|
||||
player = self.player
|
||||
world = self.multiworld
|
||||
|
||||
@@ -282,9 +286,9 @@ class BlasphemousWorld(World):
|
||||
})
|
||||
|
||||
for index, loc in enumerate(location_table):
|
||||
if not world.boots_of_pleading[player] and loc["name"] == "BotSS: 2nd meeting with Redento":
|
||||
if not options.boots_of_pleading and loc["name"] == "BotSS: 2nd meeting with Redento":
|
||||
continue
|
||||
if not world.purified_hand[player] and loc["name"] == "MoM: Western room ledge":
|
||||
if not options.purified_hand and loc["name"] == "MoM: Western room ledge":
|
||||
continue
|
||||
|
||||
region: Region = world.get_region(loc["room"], player)
|
||||
@@ -310,9 +314,9 @@ class BlasphemousWorld(World):
|
||||
victory.place_locked_item(self.create_event("Victory"))
|
||||
world.get_region("D07Z01S03", player).locations.append(victory)
|
||||
|
||||
if world.ending[self.player].value == 1:
|
||||
if options.ending.value == 1:
|
||||
set_rule(victory, lambda state: state.has("Thorn Upgrade", player, 8))
|
||||
elif world.ending[self.player].value == 2:
|
||||
elif options.ending.value == 2:
|
||||
set_rule(victory, lambda state: state.has("Thorn Upgrade", player, 8) and
|
||||
state.has("Holy Wound of Abnegation", player))
|
||||
|
||||
@@ -332,11 +336,12 @@ class BlasphemousWorld(World):
|
||||
locations = []
|
||||
doors: Dict[str, str] = {}
|
||||
|
||||
options = self.options
|
||||
world = self.multiworld
|
||||
player = self.player
|
||||
thorns: bool = True
|
||||
|
||||
if world.thorn_shuffle[player].value == 2:
|
||||
if options.thorn_shuffle.value == 2:
|
||||
thorns = False
|
||||
|
||||
for loc in world.get_filled_locations(player):
|
||||
@@ -354,28 +359,28 @@ class BlasphemousWorld(World):
|
||||
locations.append(data)
|
||||
|
||||
config = {
|
||||
"LogicDifficulty": world.difficulty[player].value,
|
||||
"StartingLocation": world.starting_location[player].value,
|
||||
"LogicDifficulty": options.difficulty.value,
|
||||
"StartingLocation": options.starting_location.value,
|
||||
"VersionCreated": "AP",
|
||||
|
||||
"UnlockTeleportation": bool(world.prie_dieu_warp[player].value),
|
||||
"AllowHints": bool(world.corpse_hints[player].value),
|
||||
"AllowPenitence": bool(world.penitence[player].value),
|
||||
"UnlockTeleportation": bool(options.prie_dieu_warp.value),
|
||||
"AllowHints": bool(options.corpse_hints.value),
|
||||
"AllowPenitence": bool(options.penitence.value),
|
||||
|
||||
"ShuffleReliquaries": bool(world.reliquary_shuffle[player].value),
|
||||
"ShuffleBootsOfPleading": bool(world.boots_of_pleading[player].value),
|
||||
"ShufflePurifiedHand": bool(world.purified_hand[player].value),
|
||||
"ShuffleDash": bool(world.dash_shuffle[player].value),
|
||||
"ShuffleWallClimb": bool(world.wall_climb_shuffle[player].value),
|
||||
"ShuffleReliquaries": bool(options.reliquary_shuffle.value),
|
||||
"ShuffleBootsOfPleading": bool(options.boots_of_pleading.value),
|
||||
"ShufflePurifiedHand": bool(options.purified_hand.value),
|
||||
"ShuffleDash": bool(options.dash_shuffle.value),
|
||||
"ShuffleWallClimb": bool(options.wall_climb_shuffle.value),
|
||||
|
||||
"ShuffleSwordSkills": bool(world.skill_randomizer[player].value),
|
||||
"ShuffleSwordSkills": bool(options.skill_randomizer.value),
|
||||
"ShuffleThorns": thorns,
|
||||
"JunkLongQuests": bool(world.skip_long_quests[player].value),
|
||||
"StartWithWheel": bool(world.start_wheel[player].value),
|
||||
"JunkLongQuests": bool(options.skip_long_quests.value),
|
||||
"StartWithWheel": bool(options.start_wheel.value),
|
||||
|
||||
"EnemyShuffleType": world.enemy_randomizer[player].value,
|
||||
"MaintainClass": bool(world.enemy_groups[player].value),
|
||||
"AreaScaling": bool(world.enemy_scaling[player].value),
|
||||
"EnemyShuffleType": options.enemy_randomizer.value,
|
||||
"MaintainClass": bool(options.enemy_groups.value),
|
||||
"AreaScaling": bool(options.enemy_scaling.value),
|
||||
|
||||
"BossShuffleType": 0,
|
||||
"DoorShuffleType": 0
|
||||
@@ -385,8 +390,8 @@ class BlasphemousWorld(World):
|
||||
"locations": locations,
|
||||
"doors": doors,
|
||||
"cfg": config,
|
||||
"ending": world.ending[self.player].value,
|
||||
"death_link": bool(world.death_link[self.player].value)
|
||||
"ending": options.ending.value,
|
||||
"death_link": bool(options.death_link.value)
|
||||
}
|
||||
|
||||
return slot_data
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
# https://opensource.org/licenses/MIT
|
||||
|
||||
import typing
|
||||
from Options import Option, Range
|
||||
from Options import Option, Range, PerGameCommonOptions
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
class TaskAdvances(Range):
|
||||
@@ -69,12 +70,12 @@ class KillerTrapWeight(Range):
|
||||
default = 0
|
||||
|
||||
|
||||
bumpstik_options: typing.Dict[str, type(Option)] = {
|
||||
"task_advances": TaskAdvances,
|
||||
"turners": Turners,
|
||||
"paint_cans": PaintCans,
|
||||
"trap_count": Traps,
|
||||
"rainbow_trap_weight": RainbowTrapWeight,
|
||||
"spinner_trap_weight": SpinnerTrapWeight,
|
||||
"killer_trap_weight": KillerTrapWeight
|
||||
}
|
||||
@dataclass
|
||||
class BumpStikOptions(PerGameCommonOptions):
|
||||
task_advances: TaskAdvances
|
||||
turners: Turners
|
||||
paint_cans: PaintCans
|
||||
trap_count: Traps
|
||||
rainbow_trap_weight: RainbowTrapWeight
|
||||
spinner_trap_weight: SpinnerTrapWeight
|
||||
killer_trap_weight: KillerTrapWeight
|
||||
|
||||
@@ -43,7 +43,8 @@ class BumpStikWorld(World):
|
||||
|
||||
required_client_version = (0, 3, 8)
|
||||
|
||||
option_definitions = bumpstik_options
|
||||
options = BumpStikOptions
|
||||
options_dataclass = BumpStikOptions
|
||||
|
||||
def __init__(self, world: MultiWorld, player: int):
|
||||
super(BumpStikWorld, self).__init__(world, player)
|
||||
@@ -86,13 +87,13 @@ class BumpStikWorld(World):
|
||||
return "Nothing"
|
||||
|
||||
def generate_early(self):
|
||||
self.task_advances = self.multiworld.task_advances[self.player].value
|
||||
self.turners = self.multiworld.turners[self.player].value
|
||||
self.paint_cans = self.multiworld.paint_cans[self.player].value
|
||||
self.traps = self.multiworld.trap_count[self.player].value
|
||||
self.rainbow_trap_weight = self.multiworld.rainbow_trap_weight[self.player].value
|
||||
self.spinner_trap_weight = self.multiworld.spinner_trap_weight[self.player].value
|
||||
self.killer_trap_weight = self.multiworld.killer_trap_weight[self.player].value
|
||||
self.task_advances = self.options.task_advances.value
|
||||
self.turners = self.options.turners.value
|
||||
self.paint_cans = self.options.paint_cans.value
|
||||
self.traps = self.options.trap_count.value
|
||||
self.rainbow_trap_weight = self.options.rainbow_trap_weight.value
|
||||
self.spinner_trap_weight = self.options.spinner_trap_weight.value
|
||||
self.killer_trap_weight = self.options.killer_trap_weight.value
|
||||
|
||||
def create_regions(self):
|
||||
create_regions(self.multiworld, self.player)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import typing
|
||||
from Options import Option
|
||||
from Options import Option, PerGameCommonOptions
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
checksfinder_options: typing.Dict[str, type(Option)] = {
|
||||
}
|
||||
@dataclass
|
||||
class ChecksFinderOptions(PerGameCommonOptions):
|
||||
pass
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
from BaseClasses import Region, Entrance, Item, Tutorial, ItemClassification
|
||||
from .Items import ChecksFinderItem, item_table, required_items
|
||||
from .Locations import ChecksFinderAdvancement, advancement_table, exclusion_table
|
||||
from .Options import checksfinder_options
|
||||
from .Options import ChecksFinderOptions
|
||||
from .Rules import set_rules, set_completion_rules
|
||||
from ..AutoWorld import World, WebWorld
|
||||
from dataclasses import fields
|
||||
|
||||
client_version = 7
|
||||
|
||||
@@ -26,7 +27,8 @@ class ChecksFinderWorld(World):
|
||||
with the mines! You win when you get all your items and beat the board!
|
||||
"""
|
||||
game: str = "ChecksFinder"
|
||||
option_definitions = checksfinder_options
|
||||
options = ChecksFinderOptions
|
||||
options_dataclass = ChecksFinderOptions
|
||||
topology_present = True
|
||||
web = ChecksFinderWeb()
|
||||
|
||||
@@ -79,8 +81,8 @@ class ChecksFinderWorld(World):
|
||||
|
||||
def fill_slot_data(self):
|
||||
slot_data = self._get_checksfinder_data()
|
||||
for option_name in checksfinder_options:
|
||||
option = getattr(self.multiworld, option_name)[self.player]
|
||||
for option_name in [field.name for field in fields(ChecksFinderOptions)]:
|
||||
option = getattr(self.options, option_name)
|
||||
if slot_data.get(option_name, None) is None and type(option.value) in {str, int}:
|
||||
slot_data[option_name] = int(option.value)
|
||||
return slot_data
|
||||
|
||||
@@ -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,6 +1,7 @@
|
||||
from typing import Callable, Dict, NamedTuple, Optional
|
||||
|
||||
from BaseClasses import Item, ItemClassification, MultiWorld
|
||||
from BaseClasses import Item, ItemClassification
|
||||
from .Options import CliqueOptions
|
||||
|
||||
|
||||
class CliqueItem(Item):
|
||||
@@ -10,7 +11,7 @@ class CliqueItem(Item):
|
||||
class CliqueItemData(NamedTuple):
|
||||
code: Optional[int] = None
|
||||
type: ItemClassification = ItemClassification.filler
|
||||
can_create: Callable[[MultiWorld, int], bool] = lambda multiworld, player: True
|
||||
can_create: Callable[[CliqueOptions], bool] = lambda options: True
|
||||
|
||||
|
||||
item_data_table: Dict[str, CliqueItemData] = {
|
||||
@@ -21,11 +22,11 @@ item_data_table: Dict[str, CliqueItemData] = {
|
||||
"Button Activation": CliqueItemData(
|
||||
code=69696968,
|
||||
type=ItemClassification.progression,
|
||||
can_create=lambda multiworld, player: bool(getattr(multiworld, "hard_mode")[player]),
|
||||
can_create=lambda options: bool(getattr(options, "hard_mode")),
|
||||
),
|
||||
"A Cool Filler Item (No Satisfaction Guaranteed)": CliqueItemData(
|
||||
code=69696967,
|
||||
can_create=lambda multiworld, player: False # Only created from `get_filler_item_name`.
|
||||
can_create=lambda options: False # Only created from `get_filler_item_name`.
|
||||
),
|
||||
"The Urge to Push": CliqueItemData(
|
||||
type=ItemClassification.progression,
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
from typing import Callable, Dict, NamedTuple, Optional
|
||||
|
||||
from BaseClasses import Location, MultiWorld
|
||||
from BaseClasses import Location
|
||||
from .Options import CliqueOptions
|
||||
|
||||
|
||||
|
||||
class CliqueLocation(Location):
|
||||
@@ -10,7 +12,7 @@ class CliqueLocation(Location):
|
||||
class CliqueLocationData(NamedTuple):
|
||||
region: str
|
||||
address: Optional[int] = None
|
||||
can_create: Callable[[MultiWorld, int], bool] = lambda multiworld, player: True
|
||||
can_create: Callable[[CliqueOptions], bool] = lambda options: True
|
||||
locked_item: Optional[str] = None
|
||||
|
||||
|
||||
@@ -22,7 +24,7 @@ location_data_table: Dict[str, CliqueLocationData] = {
|
||||
"The Item on the Desk": CliqueLocationData(
|
||||
region="The Button Realm",
|
||||
address=69696968,
|
||||
can_create=lambda multiworld, player: bool(getattr(multiworld, "hard_mode")[player]),
|
||||
can_create=lambda options: bool(getattr(options, "hard_mode")),
|
||||
),
|
||||
"In the Player's Mind": CliqueLocationData(
|
||||
region="The Button Realm",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from typing import Dict
|
||||
|
||||
from Options import Choice, Option, Toggle
|
||||
from Options import Choice, Option, Toggle, PerGameCommonOptions
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
class HardMode(Toggle):
|
||||
@@ -25,10 +26,12 @@ class ButtonColor(Choice):
|
||||
option_black = 11
|
||||
|
||||
|
||||
clique_options: Dict[str, type(Option)] = {
|
||||
"color": ButtonColor,
|
||||
"hard_mode": HardMode,
|
||||
|
||||
@dataclass
|
||||
class CliqueOptions(PerGameCommonOptions):
|
||||
color: ButtonColor
|
||||
hard_mode: HardMode
|
||||
|
||||
# DeathLink is always on. Always.
|
||||
# "death_link": DeathLink,
|
||||
}
|
||||
# death_link: DeathLink
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
from typing import Callable
|
||||
|
||||
from BaseClasses import CollectionState, MultiWorld
|
||||
from BaseClasses import CollectionState
|
||||
from .Options import CliqueOptions
|
||||
|
||||
|
||||
def get_button_rule(multiworld: MultiWorld, player: int) -> Callable[[CollectionState], bool]:
|
||||
if getattr(multiworld, "hard_mode")[player]:
|
||||
def get_button_rule(options: CliqueOptions, player: int) -> Callable[[CollectionState], bool]:
|
||||
if getattr(options, "hard_mode"):
|
||||
return lambda state: state.has("Button Activation", player)
|
||||
|
||||
return lambda state: True
|
||||
|
||||
@@ -4,33 +4,23 @@ from BaseClasses import Region, Tutorial
|
||||
from worlds.AutoWorld import WebWorld, World
|
||||
from .Items import CliqueItem, item_data_table, item_table
|
||||
from .Locations import CliqueLocation, location_data_table, location_table, locked_locations
|
||||
from .Options import clique_options
|
||||
from .Options import CliqueOptions
|
||||
from .Regions import region_data_table
|
||||
from .Rules import get_button_rule
|
||||
|
||||
|
||||
class CliqueWebWorld(WebWorld):
|
||||
theme = "partyTime"
|
||||
|
||||
setup_en = Tutorial(
|
||||
tutorial_name="Start Guide",
|
||||
description="A guide to playing Clique.",
|
||||
language="English",
|
||||
file_name="guide_en.md",
|
||||
link="guide/en",
|
||||
authors=["Phar"]
|
||||
)
|
||||
|
||||
setup_de = Tutorial(
|
||||
tutorial_name="Anleitung zum Anfangen",
|
||||
description="Eine Anleitung um Clique zu spielen.",
|
||||
language="Deutsch",
|
||||
file_name="guide_de.md",
|
||||
link="guide/de",
|
||||
authors=["Held_der_Zeit"]
|
||||
)
|
||||
|
||||
tutorials = [setup_en, setup_de]
|
||||
tutorials = [
|
||||
Tutorial(
|
||||
tutorial_name="Start Guide",
|
||||
description="A guide to playing Clique.",
|
||||
language="English",
|
||||
file_name="guide_en.md",
|
||||
link="guide/en",
|
||||
authors=["Phar"]
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
class CliqueWorld(World):
|
||||
@@ -39,7 +29,8 @@ class CliqueWorld(World):
|
||||
game = "Clique"
|
||||
data_version = 3
|
||||
web = CliqueWebWorld()
|
||||
option_definitions = clique_options
|
||||
options = CliqueOptions
|
||||
options_dataclass = CliqueOptions
|
||||
location_name_to_id = location_table
|
||||
item_name_to_id = item_table
|
||||
|
||||
@@ -49,7 +40,7 @@ class CliqueWorld(World):
|
||||
def create_items(self) -> None:
|
||||
item_pool: List[CliqueItem] = []
|
||||
for name, item in item_data_table.items():
|
||||
if item.code and item.can_create(self.multiworld, self.player):
|
||||
if item.code and item.can_create(self.options):
|
||||
item_pool.append(self.create_item(name))
|
||||
|
||||
self.multiworld.itempool += item_pool
|
||||
@@ -65,27 +56,27 @@ class CliqueWorld(World):
|
||||
region = self.multiworld.get_region(region_name, self.player)
|
||||
region.add_locations({
|
||||
location_name: location_data.address for location_name, location_data in location_data_table.items()
|
||||
if location_data.region == region_name and location_data.can_create(self.multiworld, self.player)
|
||||
if location_data.region == region_name and location_data.can_create(self.options)
|
||||
}, CliqueLocation)
|
||||
region.add_exits(region_data_table[region_name].connecting_regions)
|
||||
|
||||
# Place locked locations.
|
||||
for location_name, location_data in locked_locations.items():
|
||||
# Ignore locations we never created.
|
||||
if not location_data.can_create(self.multiworld, self.player):
|
||||
if not location_data.can_create(self.options):
|
||||
continue
|
||||
|
||||
locked_item = self.create_item(location_data_table[location_name].locked_item)
|
||||
self.multiworld.get_location(location_name, self.player).place_locked_item(locked_item)
|
||||
|
||||
# Set priority location for the Big Red Button!
|
||||
self.multiworld.priority_locations[self.player].value.add("The Big Red Button")
|
||||
self.options.priority_locations.value.add("The Big Red Button")
|
||||
|
||||
def get_filler_item_name(self) -> str:
|
||||
return "A Cool Filler Item (No Satisfaction Guaranteed)"
|
||||
|
||||
def set_rules(self) -> None:
|
||||
button_rule = get_button_rule(self.multiworld, self.player)
|
||||
button_rule = get_button_rule(self.options, self.player)
|
||||
self.multiworld.get_location("The Big Red Button", self.player).access_rule = button_rule
|
||||
self.multiworld.get_location("In the Player's Mind", self.player).access_rule = button_rule
|
||||
|
||||
@@ -98,5 +89,5 @@ class CliqueWorld(World):
|
||||
|
||||
def fill_slot_data(self):
|
||||
return {
|
||||
"color": getattr(self.multiworld, "color")[self.player].current_key
|
||||
"color": getattr(self.options, "color").current_key
|
||||
}
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
# Clique
|
||||
|
||||
## Was ist das für ein Spiel?
|
||||
|
||||
~~Clique ist ein psychologisches Überlebens-Horror Spiel, in dem der Spieler der Versuchung wiederstehen muss große~~
|
||||
~~(rote) Knöpfe zu drücken.~~
|
||||
|
||||
Clique ist ein scherzhaftes Spiel, welches für Archipelago im März 2023 entwickelt wurde, um zu zeigen, wie einfach
|
||||
es sein kann eine Welt für Archipelago zu entwicklen. Das Ziel des Spiels ist es den großen (standardmäßig) roten
|
||||
Knopf zu drücken. Wenn ein Spieler auf dem `hard_mode` (schwieriger Modus) spielt, muss dieser warten bis jemand
|
||||
anderes in der Multiworld den Knopf aktiviert, damit er gedrückt werden kann.
|
||||
|
||||
Clique kann auf den meisten modernen, HTML5-fähigen Browsern gespielt werden.
|
||||
|
||||
## Wo ist die Seite für die Einstellungen?
|
||||
|
||||
Die [Seite für die Spielereinstellungen dieses Spiels](../player-options) enthält alle Optionen die man benötigt um
|
||||
eine YAML-Datei zu konfigurieren und zu exportieren.
|
||||
@@ -1,25 +0,0 @@
|
||||
# Clique Anleitung
|
||||
|
||||
Nachdem dein Seed generiert wurde, gehe auf die Website von [Clique dem Spiel](http://clique.pharware.com/) und gib
|
||||
Server-Daten, deinen Slot-Namen und ein Passwort (falls vorhanden) ein. Klicke dann auf "Connect" (Verbinden).
|
||||
|
||||
Wenn du auf "Einfach" spielst, kannst du unbedenklich den Knopf drücken und deine "Befriedigung" erhalten.
|
||||
|
||||
Wenn du auf "Schwer" spielst, ist es sehr wahrscheinlich, dass du warten musst bevor du dein Ziel erreichen kannst.
|
||||
Glücklicherweise läuft Click auf den meißten großen Browsern, die HTML5 unterstützen. Das heißt du kannst Clique auf
|
||||
deinem Handy starten und produktiv sein während du wartest!
|
||||
|
||||
Falls du einige Ideen brauchst was du tun kannst, während du wartest bis der Knopf aktiviert wurde, versuche
|
||||
(mindestens) eins der Folgenden:
|
||||
|
||||
- Dein Zimmer aufräumen.
|
||||
- Die Wäsche machen.
|
||||
- Etwas Essen von einem X-Belieben Fast Food Restaruant holen.
|
||||
- Das tägliche Wordle machen.
|
||||
- ~~Deine Seele an **Phar** verkaufen.~~
|
||||
- Deine Hausaufgaben erledigen.
|
||||
- Deine Post abholen.
|
||||
|
||||
|
||||
~~Solltest du auf irgendwelche Probleme in diesem Spiel stoßen, solltest du keinesfalls nicht **thephar** auf~~
|
||||
~~Discord kontaktieren. *zwinker* *zwinker*~~
|
||||
@@ -1,6 +1,7 @@
|
||||
import typing
|
||||
|
||||
from Options import Toggle, DefaultOnToggle, Option, Range, Choice, ItemDict, DeathLink
|
||||
from Options import Toggle, DefaultOnToggle, Option, Range, Choice, ItemDict, DeathLink, PerGameCommonOptions
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
class RandomizeWeaponLocations(DefaultOnToggle):
|
||||
@@ -200,36 +201,36 @@ class EnableDLCOption(Toggle):
|
||||
display_name = "Enable DLC"
|
||||
|
||||
|
||||
dark_souls_options: typing.Dict[str, Option] = {
|
||||
"enable_weapon_locations": RandomizeWeaponLocations,
|
||||
"enable_shield_locations": RandomizeShieldLocations,
|
||||
"enable_armor_locations": RandomizeArmorLocations,
|
||||
"enable_ring_locations": RandomizeRingLocations,
|
||||
"enable_spell_locations": RandomizeSpellLocations,
|
||||
"enable_key_locations": RandomizeKeyLocations,
|
||||
"enable_boss_locations": RandomizeBossSoulLocations,
|
||||
"enable_npc_locations": RandomizeNPCLocations,
|
||||
"enable_misc_locations": RandomizeMiscLocations,
|
||||
"enable_health_upgrade_locations": RandomizeHealthLocations,
|
||||
"enable_progressive_locations": RandomizeProgressiveLocationsOption,
|
||||
"pool_type": PoolTypeOption,
|
||||
"guaranteed_items": GuaranteedItemsOption,
|
||||
"auto_equip": AutoEquipOption,
|
||||
"lock_equip": LockEquipOption,
|
||||
"no_weapon_requirements": NoWeaponRequirementsOption,
|
||||
"randomize_infusion": RandomizeInfusionOption,
|
||||
"randomize_infusion_percentage": RandomizeInfusionPercentageOption,
|
||||
"randomize_weapon_level": RandomizeWeaponLevelOption,
|
||||
"randomize_weapon_level_percentage": RandomizeWeaponLevelPercentageOption,
|
||||
"min_levels_in_5": MinLevelsIn5WeaponPoolOption,
|
||||
"max_levels_in_5": MaxLevelsIn5WeaponPoolOption,
|
||||
"min_levels_in_10": MinLevelsIn10WeaponPoolOption,
|
||||
"max_levels_in_10": MaxLevelsIn10WeaponPoolOption,
|
||||
"early_banner": EarlySmallLothricBanner,
|
||||
"late_basin_of_vows": LateBasinOfVowsOption,
|
||||
"late_dlc": LateDLCOption,
|
||||
"no_spell_requirements": NoSpellRequirementsOption,
|
||||
"no_equip_load": NoEquipLoadOption,
|
||||
"death_link": DeathLink,
|
||||
"enable_dlc": EnableDLCOption,
|
||||
}
|
||||
@dataclass
|
||||
class DarkSouls3Options(PerGameCommonOptions):
|
||||
enable_weapon_locations: RandomizeWeaponLocations
|
||||
enable_shield_locations: RandomizeShieldLocations
|
||||
enable_armor_locations: RandomizeArmorLocations
|
||||
enable_ring_locations: RandomizeRingLocations
|
||||
enable_spell_locations: RandomizeSpellLocations
|
||||
enable_key_locations: RandomizeKeyLocations
|
||||
enable_boss_locations: RandomizeBossSoulLocations
|
||||
enable_npc_locations: RandomizeNPCLocations
|
||||
enable_misc_locations: RandomizeMiscLocations
|
||||
enable_health_upgrade_locations: RandomizeHealthLocations
|
||||
enable_progressive_locations: RandomizeProgressiveLocationsOption
|
||||
pool_type: PoolTypeOption
|
||||
guaranteed_items: GuaranteedItemsOption
|
||||
auto_equip: AutoEquipOption
|
||||
lock_equip: LockEquipOption
|
||||
no_weapon_requirements: NoWeaponRequirementsOption
|
||||
randomize_infusion: RandomizeInfusionOption
|
||||
randomize_infusion_percentage: RandomizeInfusionPercentageOption
|
||||
randomize_weapon_level: RandomizeWeaponLevelOption
|
||||
randomize_weapon_level_percentage: RandomizeWeaponLevelPercentageOption
|
||||
min_levels_in_5: MinLevelsIn5WeaponPoolOption
|
||||
max_levels_in_5: MaxLevelsIn5WeaponPoolOption
|
||||
min_levels_in_10: MinLevelsIn10WeaponPoolOption
|
||||
max_levels_in_10: MaxLevelsIn10WeaponPoolOption
|
||||
early_banner: EarlySmallLothricBanner
|
||||
late_basin_of_vows: LateBasinOfVowsOption
|
||||
late_dlc: LateDLCOption
|
||||
no_spell_requirements: NoSpellRequirementsOption
|
||||
no_equip_load: NoEquipLoadOption
|
||||
death_link: DeathLink
|
||||
enable_dlc: EnableDLCOption
|
||||
|
||||
@@ -9,7 +9,7 @@ from worlds.generic.Rules import set_rule, add_rule, add_item_rule
|
||||
|
||||
from .Items import DarkSouls3Item, DS3ItemCategory, item_dictionary, key_item_names, item_descriptions
|
||||
from .Locations import DarkSouls3Location, DS3LocationCategory, location_tables, location_dictionary
|
||||
from .Options import RandomizeWeaponLevelOption, PoolTypeOption, EarlySmallLothricBanner, dark_souls_options
|
||||
from .Options import RandomizeWeaponLevelOption, PoolTypeOption, EarlySmallLothricBanner, DarkSouls3Options
|
||||
|
||||
|
||||
class DarkSouls3Web(WebWorld):
|
||||
@@ -43,7 +43,8 @@ class DarkSouls3World(World):
|
||||
"""
|
||||
|
||||
game: str = "Dark Souls III"
|
||||
option_definitions = dark_souls_options
|
||||
options = DarkSouls3Options
|
||||
options_dataclass = DarkSouls3Options
|
||||
topology_present: bool = True
|
||||
web = DarkSouls3Web()
|
||||
data_version = 8
|
||||
@@ -72,47 +73,47 @@ class DarkSouls3World(World):
|
||||
|
||||
|
||||
def generate_early(self):
|
||||
if self.multiworld.enable_weapon_locations[self.player] == Toggle.option_true:
|
||||
if self.options.enable_weapon_locations == Toggle.option_true:
|
||||
self.enabled_location_categories.add(DS3LocationCategory.WEAPON)
|
||||
if self.multiworld.enable_shield_locations[self.player] == Toggle.option_true:
|
||||
if self.options.enable_shield_locations == Toggle.option_true:
|
||||
self.enabled_location_categories.add(DS3LocationCategory.SHIELD)
|
||||
if self.multiworld.enable_armor_locations[self.player] == Toggle.option_true:
|
||||
if self.options.enable_armor_locations == Toggle.option_true:
|
||||
self.enabled_location_categories.add(DS3LocationCategory.ARMOR)
|
||||
if self.multiworld.enable_ring_locations[self.player] == Toggle.option_true:
|
||||
if self.options.enable_ring_locations == Toggle.option_true:
|
||||
self.enabled_location_categories.add(DS3LocationCategory.RING)
|
||||
if self.multiworld.enable_spell_locations[self.player] == Toggle.option_true:
|
||||
if self.options.enable_spell_locations == Toggle.option_true:
|
||||
self.enabled_location_categories.add(DS3LocationCategory.SPELL)
|
||||
if self.multiworld.enable_npc_locations[self.player] == Toggle.option_true:
|
||||
if self.options.enable_npc_locations == Toggle.option_true:
|
||||
self.enabled_location_categories.add(DS3LocationCategory.NPC)
|
||||
if self.multiworld.enable_key_locations[self.player] == Toggle.option_true:
|
||||
if self.options.enable_key_locations == Toggle.option_true:
|
||||
self.enabled_location_categories.add(DS3LocationCategory.KEY)
|
||||
if self.multiworld.early_banner[self.player] == EarlySmallLothricBanner.option_early_global:
|
||||
self.multiworld.early_items[self.player]['Small Lothric Banner'] = 1
|
||||
elif self.multiworld.early_banner[self.player] == EarlySmallLothricBanner.option_early_local:
|
||||
self.multiworld.local_early_items[self.player]['Small Lothric Banner'] = 1
|
||||
if self.multiworld.enable_boss_locations[self.player] == Toggle.option_true:
|
||||
if self.options.early_banner == EarlySmallLothricBanner.option_early_global:
|
||||
self.options.early_items['Small Lothric Banner'] = 1
|
||||
elif self.options.early_banner == EarlySmallLothricBanner.option_early_local:
|
||||
self.options.local_early_items['Small Lothric Banner'] = 1
|
||||
if self.options.enable_boss_locations == Toggle.option_true:
|
||||
self.enabled_location_categories.add(DS3LocationCategory.BOSS)
|
||||
if self.multiworld.enable_misc_locations[self.player] == Toggle.option_true:
|
||||
if self.options.enable_misc_locations == Toggle.option_true:
|
||||
self.enabled_location_categories.add(DS3LocationCategory.MISC)
|
||||
if self.multiworld.enable_health_upgrade_locations[self.player] == Toggle.option_true:
|
||||
if self.options.enable_health_upgrade_locations == Toggle.option_true:
|
||||
self.enabled_location_categories.add(DS3LocationCategory.HEALTH)
|
||||
if self.multiworld.enable_progressive_locations[self.player] == Toggle.option_true:
|
||||
if self.options.enable_progressive_locations == Toggle.option_true:
|
||||
self.enabled_location_categories.add(DS3LocationCategory.PROGRESSIVE_ITEM)
|
||||
|
||||
|
||||
def create_regions(self):
|
||||
progressive_location_table = []
|
||||
if self.multiworld.enable_progressive_locations[self.player]:
|
||||
if self.options.enable_progressive_locations:
|
||||
progressive_location_table = [] + \
|
||||
location_tables["Progressive Items 1"] + \
|
||||
location_tables["Progressive Items 2"] + \
|
||||
location_tables["Progressive Items 3"] + \
|
||||
location_tables["Progressive Items 4"]
|
||||
|
||||
if self.multiworld.enable_dlc[self.player].value:
|
||||
if self.options.enable_dlc.value:
|
||||
progressive_location_table += location_tables["Progressive Items DLC"]
|
||||
|
||||
if self.multiworld.enable_health_upgrade_locations[self.player]:
|
||||
if self.options.enable_health_upgrade_locations:
|
||||
progressive_location_table += location_tables["Progressive Items Health"]
|
||||
|
||||
# Create Vanilla Regions
|
||||
@@ -146,7 +147,7 @@ class DarkSouls3World(World):
|
||||
regions["Consumed King's Garden"].locations.append(potd_location)
|
||||
|
||||
# Create DLC Regions
|
||||
if self.multiworld.enable_dlc[self.player]:
|
||||
if self.options.enable_dlc:
|
||||
regions.update({region_name: self.create_region(region_name, location_tables[region_name]) for region_name in [
|
||||
"Painted World of Ariandel 1",
|
||||
"Painted World of Ariandel 2",
|
||||
@@ -192,7 +193,7 @@ class DarkSouls3World(World):
|
||||
create_connection("Consumed King's Garden", "Untended Graves")
|
||||
|
||||
# Connect DLC Regions
|
||||
if self.multiworld.enable_dlc[self.player]:
|
||||
if self.options.enable_dlc:
|
||||
create_connection("Cathedral of the Deep", "Painted World of Ariandel 1")
|
||||
create_connection("Painted World of Ariandel 1", "Painted World of Ariandel 2")
|
||||
create_connection("Painted World of Ariandel 2", "Dreg Heap")
|
||||
@@ -240,7 +241,7 @@ class DarkSouls3World(World):
|
||||
|
||||
|
||||
def create_items(self):
|
||||
dlc_enabled = self.multiworld.enable_dlc[self.player] == Toggle.option_true
|
||||
dlc_enabled = self.options.enable_dlc == Toggle.option_true
|
||||
|
||||
itempool_by_category = {category: [] for category in self.enabled_location_categories}
|
||||
|
||||
@@ -254,7 +255,7 @@ class DarkSouls3World(World):
|
||||
itempool_by_category[location.category].append(location.default_item_name)
|
||||
|
||||
# Replace each item category with a random sample of items of those types
|
||||
if self.multiworld.pool_type[self.player] == PoolTypeOption.option_various:
|
||||
if self.options.pool_type == PoolTypeOption.option_various:
|
||||
def create_random_replacement_list(item_categories: Set[DS3ItemCategory], num_items: int):
|
||||
candidates = [
|
||||
item.name for item
|
||||
@@ -300,7 +301,7 @@ class DarkSouls3World(World):
|
||||
# A list of items we can replace
|
||||
removable_items = [item for item in itempool if item.classification != ItemClassification.progression]
|
||||
|
||||
guaranteed_items = self.multiworld.guaranteed_items[self.player].value
|
||||
guaranteed_items = self.options.guaranteed_items.value
|
||||
for item_name in guaranteed_items:
|
||||
# Break early just in case nothing is removable (if user is trying to guarantee more
|
||||
# items than the pool can hold, for example)
|
||||
@@ -384,22 +385,22 @@ class DarkSouls3World(World):
|
||||
state.has("Cinders of a Lord - Aldrich", self.player) and
|
||||
state.has("Cinders of a Lord - Lothric Prince", self.player))
|
||||
|
||||
if self.multiworld.late_basin_of_vows[self.player] == Toggle.option_true:
|
||||
if self.options.late_basin_of_vows == Toggle.option_true:
|
||||
add_rule(self.multiworld.get_entrance("Go To Lothric Castle", self.player),
|
||||
lambda state: state.has("Small Lothric Banner", self.player))
|
||||
|
||||
# DLC Access Rules Below
|
||||
if self.multiworld.enable_dlc[self.player]:
|
||||
if self.options.enable_dlc:
|
||||
set_rule(self.multiworld.get_entrance("Go To Ringed City", self.player),
|
||||
lambda state: state.has("Small Envoy Banner", self.player))
|
||||
|
||||
# If key items are randomized, must have contraption key to enter second half of Ashes DLC
|
||||
# If key items are not randomized, Contraption Key is guaranteed to be accessible before it is needed
|
||||
if self.multiworld.enable_key_locations[self.player] == Toggle.option_true:
|
||||
if self.options.enable_key_locations == Toggle.option_true:
|
||||
add_rule(self.multiworld.get_entrance("Go To Painted World of Ariandel 2", self.player),
|
||||
lambda state: state.has("Contraption Key", self.player))
|
||||
|
||||
if self.multiworld.late_dlc[self.player] == Toggle.option_true:
|
||||
if self.options.late_dlc == Toggle.option_true:
|
||||
add_rule(self.multiworld.get_entrance("Go To Painted World of Ariandel 1", self.player),
|
||||
lambda state: state.has("Small Doll", self.player))
|
||||
|
||||
@@ -407,7 +408,7 @@ class DarkSouls3World(World):
|
||||
set_rule(self.multiworld.get_location("PC: Cinders of a Lord - Yhorm the Giant", self.player),
|
||||
lambda state: state.has("Storm Ruler", self.player))
|
||||
|
||||
if self.multiworld.enable_ring_locations[self.player] == Toggle.option_true:
|
||||
if self.options.enable_ring_locations == Toggle.option_true:
|
||||
set_rule(self.multiworld.get_location("ID: Bellowing Dragoncrest Ring", self.player),
|
||||
lambda state: state.has("Jailbreaker's Key", self.player))
|
||||
set_rule(self.multiworld.get_location("ID: Covetous Gold Serpent Ring", self.player),
|
||||
@@ -415,7 +416,7 @@ class DarkSouls3World(World):
|
||||
set_rule(self.multiworld.get_location("UG: Hornet Ring", self.player),
|
||||
lambda state: state.has("Small Lothric Banner", self.player))
|
||||
|
||||
if self.multiworld.enable_npc_locations[self.player] == Toggle.option_true:
|
||||
if self.options.enable_npc_locations == Toggle.option_true:
|
||||
set_rule(self.multiworld.get_location("HWL: Greirat's Ashes", self.player),
|
||||
lambda state: state.has("Cell Key", self.player))
|
||||
set_rule(self.multiworld.get_location("HWL: Blue Tearstone Ring", self.player),
|
||||
@@ -431,11 +432,11 @@ class DarkSouls3World(World):
|
||||
set_rule(self.multiworld.get_location("ID: Karla's Trousers", self.player),
|
||||
lambda state: state.has("Jailer's Key Ring", self.player))
|
||||
|
||||
if self.multiworld.enable_misc_locations[self.player] == Toggle.option_true:
|
||||
if self.options.enable_misc_locations == Toggle.option_true:
|
||||
set_rule(self.multiworld.get_location("ID: Prisoner Chief's Ashes", self.player),
|
||||
lambda state: state.has("Jailer's Key Ring", self.player))
|
||||
|
||||
if self.multiworld.enable_boss_locations[self.player] == Toggle.option_true:
|
||||
if self.options.enable_boss_locations == Toggle.option_true:
|
||||
set_rule(self.multiworld.get_location("PC: Soul of Yhorm the Giant", self.player),
|
||||
lambda state: state.has("Storm Ruler", self.player))
|
||||
set_rule(self.multiworld.get_location("HWL: Soul of the Dancer", self.player),
|
||||
@@ -443,7 +444,7 @@ class DarkSouls3World(World):
|
||||
|
||||
# Lump Soul of the Dancer in with LC for locations that should not be reachable
|
||||
# before having access to US. (Prevents requiring getting Basin to fight Dancer to get SLB to go to US)
|
||||
if self.multiworld.late_basin_of_vows[self.player] == Toggle.option_true:
|
||||
if self.options.late_basin_of_vows == Toggle.option_true:
|
||||
add_rule(self.multiworld.get_location("HWL: Soul of the Dancer", self.player),
|
||||
lambda state: state.has("Small Lothric Banner", self.player))
|
||||
|
||||
@@ -453,10 +454,10 @@ class DarkSouls3World(World):
|
||||
|
||||
set_rule(self.multiworld.get_location("LC: Grand Archives Key", self.player), gotthard_corpse_rule)
|
||||
|
||||
if self.multiworld.enable_weapon_locations[self.player] == Toggle.option_true:
|
||||
if self.options.enable_weapon_locations == Toggle.option_true:
|
||||
set_rule(self.multiworld.get_location("LC: Gotthard Twinswords", self.player), gotthard_corpse_rule)
|
||||
|
||||
self.multiworld.completion_condition[self.player] = lambda state: \
|
||||
self.options.completion_condition = lambda state: \
|
||||
state.has("Cinders of a Lord - Abyss Watcher", self.player) and \
|
||||
state.has("Cinders of a Lord - Yhorm the Giant", self.player) and \
|
||||
state.has("Cinders of a Lord - Aldrich", self.player) and \
|
||||
@@ -470,13 +471,13 @@ class DarkSouls3World(World):
|
||||
name_to_ds3_code = {item.name: item.ds3_code for item in item_dictionary.values()}
|
||||
|
||||
# Randomize some weapon upgrades
|
||||
if self.multiworld.randomize_weapon_level[self.player] != RandomizeWeaponLevelOption.option_none:
|
||||
if self.options.randomize_weapon_level != RandomizeWeaponLevelOption.option_none:
|
||||
# if the user made an error and set a min higher than the max we default to the max
|
||||
max_5 = self.multiworld.max_levels_in_5[self.player]
|
||||
min_5 = min(self.multiworld.min_levels_in_5[self.player], max_5)
|
||||
max_10 = self.multiworld.max_levels_in_10[self.player]
|
||||
min_10 = min(self.multiworld.min_levels_in_10[self.player], max_10)
|
||||
weapon_level_percentage = self.multiworld.randomize_weapon_level_percentage[self.player]
|
||||
max_5 = self.options.max_levels_in_5
|
||||
min_5 = min(self.options.min_levels_in_5, max_5)
|
||||
max_10 = self.options.max_levels_in_10
|
||||
min_10 = min(self.options.min_levels_in_10, max_10)
|
||||
weapon_level_percentage = self.options.randomize_weapon_level_percentage
|
||||
|
||||
for item in item_dictionary.values():
|
||||
if self.multiworld.per_slot_randoms[self.player].randint(0, 99) < weapon_level_percentage:
|
||||
@@ -486,8 +487,8 @@ class DarkSouls3World(World):
|
||||
name_to_ds3_code[item.name] += self.multiworld.per_slot_randoms[self.player].randint(min_10, max_10)
|
||||
|
||||
# Randomize some weapon infusions
|
||||
if self.multiworld.randomize_infusion[self.player] == Toggle.option_true:
|
||||
infusion_percentage = self.multiworld.randomize_infusion_percentage[self.player]
|
||||
if self.options.randomize_infusion == Toggle.option_true:
|
||||
infusion_percentage = self.options.randomize_infusion_percentage
|
||||
for item in item_dictionary.values():
|
||||
if item.category in {DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE, DS3ItemCategory.SHIELD_INFUSIBLE}:
|
||||
if self.multiworld.per_slot_randoms[self.player].randint(0, 99) < infusion_percentage:
|
||||
@@ -518,22 +519,22 @@ class DarkSouls3World(World):
|
||||
|
||||
slot_data = {
|
||||
"options": {
|
||||
"enable_weapon_locations": self.multiworld.enable_weapon_locations[self.player].value,
|
||||
"enable_shield_locations": self.multiworld.enable_shield_locations[self.player].value,
|
||||
"enable_armor_locations": self.multiworld.enable_armor_locations[self.player].value,
|
||||
"enable_ring_locations": self.multiworld.enable_ring_locations[self.player].value,
|
||||
"enable_spell_locations": self.multiworld.enable_spell_locations[self.player].value,
|
||||
"enable_key_locations": self.multiworld.enable_key_locations[self.player].value,
|
||||
"enable_boss_locations": self.multiworld.enable_boss_locations[self.player].value,
|
||||
"enable_npc_locations": self.multiworld.enable_npc_locations[self.player].value,
|
||||
"enable_misc_locations": self.multiworld.enable_misc_locations[self.player].value,
|
||||
"auto_equip": self.multiworld.auto_equip[self.player].value,
|
||||
"lock_equip": self.multiworld.lock_equip[self.player].value,
|
||||
"no_weapon_requirements": self.multiworld.no_weapon_requirements[self.player].value,
|
||||
"death_link": self.multiworld.death_link[self.player].value,
|
||||
"no_spell_requirements": self.multiworld.no_spell_requirements[self.player].value,
|
||||
"no_equip_load": self.multiworld.no_equip_load[self.player].value,
|
||||
"enable_dlc": self.multiworld.enable_dlc[self.player].value
|
||||
"enable_weapon_locations": self.options.enable_weapon_locations.value,
|
||||
"enable_shield_locations": self.options.enable_shield_locations.value,
|
||||
"enable_armor_locations": self.options.enable_armor_locations.value,
|
||||
"enable_ring_locations": self.options.enable_ring_locations.value,
|
||||
"enable_spell_locations": self.options.enable_spell_locations.value,
|
||||
"enable_key_locations": self.options.enable_key_locations.value,
|
||||
"enable_boss_locations": self.options.enable_boss_locations.value,
|
||||
"enable_npc_locations": self.options.enable_npc_locations.value,
|
||||
"enable_misc_locations": self.options.enable_misc_locations.value,
|
||||
"auto_equip": self.options.auto_equip.value,
|
||||
"lock_equip": self.options.lock_equip.value,
|
||||
"no_weapon_requirements": self.options.no_weapon_requirements.value,
|
||||
"death_link": self.options.death_link.value,
|
||||
"no_spell_requirements": self.options.no_spell_requirements.value,
|
||||
"no_equip_load": self.options.no_equip_load.value,
|
||||
"enable_dlc": self.options.enable_dlc.value
|
||||
},
|
||||
"seed": self.multiworld.seed_name, # to verify the server's multiworld
|
||||
"slot": self.multiworld.player_name[self.player], # to connect to server
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -13,23 +13,14 @@ client_version = 0
|
||||
|
||||
|
||||
class DLCqwebworld(WebWorld):
|
||||
setup_en = Tutorial(
|
||||
tutorials = [Tutorial(
|
||||
"Multiworld Setup Tutorial",
|
||||
"A guide to setting up the Archipelago DLCQuest game on your computer.",
|
||||
"English",
|
||||
"setup_en.md",
|
||||
"setup/en",
|
||||
["axe_y"]
|
||||
)
|
||||
setup_fr = Tutorial(
|
||||
"Guide de configuration MultiWorld",
|
||||
"Un guide pour configurer DLCQuest sur votre PC.",
|
||||
"Français",
|
||||
"setup_fr.md",
|
||||
"setup/fr",
|
||||
["Deoxis"]
|
||||
)
|
||||
tutorials = [setup_en, setup_fr]
|
||||
)]
|
||||
|
||||
|
||||
class DLCqworld(World):
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
# DLC Quest
|
||||
|
||||
## Où se trouve la page des paramètres ?
|
||||
|
||||
La [page des paramètres du joueur pour ce jeu](../player-settings) contient tous les paramètres dont vous avez besoin pour configurer et exporter le fichier.
|
||||
|
||||
|
||||
## Quel est l'effet de la randomisation sur ce jeu ?
|
||||
|
||||
Les DLC seront obtenus en tant que check pour le multiworld. Il existe également d'autres checks optionnels dans DLC Quest.
|
||||
|
||||
## Quel est le but de DLC Quest ?
|
||||
|
||||
DLC Quest a deux campagnes, et le joueur peut choisir celle qu'il veut jouer pour sa partie.
|
||||
Il peut également choisir de faire les deux campagnes.
|
||||
|
||||
|
||||
## Quels sont les emplacements dans DLC quest ?
|
||||
|
||||
Les emplacements dans DLC Quest comprennent toujours
|
||||
- les achats de DLC auprès du commerçant
|
||||
- Les objectifs liés aux récompenses
|
||||
- Tuer des moutons dans DLC Quest
|
||||
- Objectifs spécifiques de l'attribution dans Live Freemium or Die
|
||||
|
||||
Il existe également un certain nombres de critères de localisation qui sont optionnels et que les joueurs peuvent choisir d'inclure ou non dans leur sélection :
|
||||
- Objets que votre personnage peut obtenir de différentes manières
|
||||
- Swords
|
||||
- Gun
|
||||
- Box of Various Supplies
|
||||
- Humble Indie Bindle
|
||||
- Pickaxe
|
||||
- Coinsanity : Pièces de monnaie, soit individuellement, soit sous forme de lots personnalisés
|
||||
|
||||
## Quels objets peuvent se trouver dans le monde d'un autre joueur ?
|
||||
|
||||
Tous les DLC du jeu sont mélangés dans le stock d'objets. Les objets liés aux contrôles optionnels décrits ci-dessus sont également dans le stock
|
||||
|
||||
Il y a aussi de nouveaux objets pièges, utilisés comme substituts, basés sur les désagréments du jeu vanille.
|
||||
- Zombie Sheep
|
||||
- Loading Screens
|
||||
- Temporary Spikes
|
||||
|
||||
## Que se passe-t-il lorsque le joueur reçoit un objet ?
|
||||
|
||||
Chaque fois qu'un objet est reçu en ligne, une notification apparaît à l'écran pour en informer le joueur.
|
||||
Certains objets sont accompagnés d'une animation ou d'une scène qui se déroule immédiatement après leur réception.
|
||||
|
||||
Les objets reçus hors ligne ne sont pas accompagnés d'une animation ou d'une scène, et sont simplement activés lors de la connexion.
|
||||
@@ -1,55 +0,0 @@
|
||||
# # Guide de configuration MultiWorld de DLCQuest
|
||||
|
||||
## Logiciels requis
|
||||
|
||||
- DLC Quest sur PC (Recommandé: [Version Steam](https://store.steampowered.com/app/230050/DLC_Quest/))
|
||||
- [DLCQuestipelago](https://github.com/agilbert1412/DLCQuestipelago/releases)
|
||||
- BepinEx (utilisé comme un modloader pour DLCQuest. La version du mod ci-dessus inclut BepInEx si vous choisissez la version d'installation complète)
|
||||
|
||||
## Logiciels optionnels
|
||||
- [Archipelago] (https://github.com/ArchipelagoMW/Archipelago/releases)
|
||||
- (Uniquement pour le TextClient)
|
||||
|
||||
## Créer un fichier de configuration (.yaml)
|
||||
|
||||
### Qu'est-ce qu'un fichier YAML et pourquoi en ai-je besoin ?
|
||||
|
||||
Voir le guide d'Archipelago sur la mise en place d'un YAML de base : [Basic Multiworld Setup Guide](/tutorial/Archipelago/setup/en)
|
||||
|
||||
### Où puis-je obtenir un fichier YAML ?
|
||||
|
||||
Vous pouvez personnaliser vos paramètres en visitant la [page des paramètres du joueur DLC Quest] (/games/DLCQuest/player-settings).
|
||||
|
||||
## Rejoindre une partie multi-monde
|
||||
|
||||
### Installer le mod
|
||||
|
||||
- Télécharger le [DLCQuestipelago mod release](https://github.com/agilbert1412/DLCQuestipelago/releases). Si c'est la première fois que vous installez le mod, ou si vous n'êtes pas à l'aise avec l'édition manuelle de fichiers, vous devriez choisir l'Installateur. Il se chargera de la plus grande partie du travail pour vous
|
||||
|
||||
|
||||
- Extraire l'archive .zip à l'emplacement de votre choix
|
||||
|
||||
|
||||
- Exécutez "DLCQuestipelagoInstaller.exe".
|
||||
|
||||

|
||||
- Le programme d'installation devrait décrire ce qu'il fait à chaque étape, et vous demandera votre avis si nécessaire.
|
||||
- Il vous permettra de choisir l'emplacement d'installation de votre jeu moddé et vous proposera un emplacement par défaut
|
||||
- Il **essayera** de trouver votre jeu DLCQuest sur votre ordinateur et, en cas d'échec, vous demandera d'indiquer le chemin d'accès.
|
||||
- Il vous offrira la possibilité de créer un raccourci sur le bureau pour le lanceur moddé.
|
||||
|
||||
### Se connecter au MultiServer
|
||||
|
||||
- Localisez le fichier "ArchipelagoConnectionInfo.json", qui se situe dans le même emplacement que votre installation moddée. Vous pouvez éditer ce fichier avec n'importe quel éditeur de texte, et vous devez entrer l'adresse IP du serveur, le port et votre nom de joueur dans les champs appropriés.
|
||||
|
||||
- Exécutez BepInEx.NET.Framework.Launcher.exe. Si vous avez opté pour un raccourci sur le bureau, vous le trouverez avec une icône et un nom plus reconnaissable.
|
||||

|
||||
|
||||
- Votre jeu devrait se lancer en même temps qu'une console de modloader, qui contiendra des informations de débogage importantes si vous rencontrez des problèmes.
|
||||
- Le jeu devrait se connecter automatiquement, et tenter de se reconnecter si votre internet ou le serveur se déconnecte, pendant que vous jouez.
|
||||
|
||||
### Interagir avec le MultiWorld depuis le jeu
|
||||
|
||||
Vous ne pouvez pas envoyer de commandes au serveur ou discuter avec les autres joueurs depuis DLC Quest, car le jeu ne dispose pas d'un moyen approprié pour saisir du texte.
|
||||
Vous pouvez suivre l'activité du serveur dans votre console BepInEx, car les messages de chat d'Archipelago y seront affichés.
|
||||
Vous devrez utiliser [Archipelago Text Client] (https://github.com/ArchipelagoMW/Archipelago/releases) si vous voulez envoyer des commandes.
|
||||
@@ -6,6 +6,8 @@ import shutil
|
||||
import threading
|
||||
import zipfile
|
||||
from typing import Optional, TYPE_CHECKING, Any, List, Callable, Tuple, Union
|
||||
from dataclasses import fields
|
||||
import datetime
|
||||
|
||||
import jinja2
|
||||
|
||||
@@ -88,6 +90,7 @@ class FactorioModFile(worlds.Files.APContainer):
|
||||
def generate_mod(world: "Factorio", output_directory: str):
|
||||
player = world.player
|
||||
multiworld = world.multiworld
|
||||
options = world.options
|
||||
global data_final_template, locale_template, control_template, data_template, settings_template
|
||||
with template_load_lock:
|
||||
if not data_final_template:
|
||||
@@ -129,40 +132,40 @@ def generate_mod(world: "Factorio", output_directory: str):
|
||||
"base_tech_table": base_tech_table,
|
||||
"tech_to_progressive_lookup": tech_to_progressive_lookup,
|
||||
"mod_name": mod_name,
|
||||
"allowed_science_packs": multiworld.max_science_pack[player].get_allowed_packs(),
|
||||
"allowed_science_packs": options.max_science_pack.get_allowed_packs(),
|
||||
"custom_technologies": multiworld.worlds[player].custom_technologies,
|
||||
"tech_tree_layout_prerequisites": world.tech_tree_layout_prerequisites,
|
||||
"slot_name": multiworld.player_name[player], "seed_name": multiworld.seed_name,
|
||||
"slot_player": player,
|
||||
"starting_items": multiworld.starting_items[player], "recipes": recipes,
|
||||
"starting_items": options.starting_items, "recipes": recipes,
|
||||
"random": random, "flop_random": flop_random,
|
||||
"recipe_time_scale": recipe_time_scales.get(multiworld.recipe_time[player].value, None),
|
||||
"recipe_time_range": recipe_time_ranges.get(multiworld.recipe_time[player].value, None),
|
||||
"recipe_time_scale": recipe_time_scales.get(options.recipe_time.value, None),
|
||||
"recipe_time_range": recipe_time_ranges.get(options.recipe_time.value, None),
|
||||
"free_sample_blacklist": {item: 1 for item in free_sample_exclusions},
|
||||
"progressive_technology_table": {tech.name: tech.progressive for tech in
|
||||
progressive_technology_table.values()},
|
||||
"custom_recipes": world.custom_recipes,
|
||||
"max_science_pack": multiworld.max_science_pack[player].value,
|
||||
"max_science_pack": options.max_science_pack.value,
|
||||
"liquids": fluids,
|
||||
"goal": multiworld.goal[player].value,
|
||||
"energy_link": multiworld.energy_link[player].value,
|
||||
"goal": options.goal.value,
|
||||
"energy_link": options.energy_link.value,
|
||||
"useless_technologies": useless_technologies,
|
||||
"chunk_shuffle": multiworld.chunk_shuffle[player].value if hasattr(multiworld, "chunk_shuffle") else 0,
|
||||
"chunk_shuffle": options.chunk_shuffle.value if datetime.datetime.today().month == 4 else 0,
|
||||
}
|
||||
|
||||
for factorio_option in Options.factorio_options:
|
||||
for factorio_option in [field.name for field in fields(Options.FactorioOptions)]:
|
||||
if factorio_option in ["free_sample_blacklist", "free_sample_whitelist"]:
|
||||
continue
|
||||
template_data[factorio_option] = getattr(multiworld, factorio_option)[player].value
|
||||
template_data[factorio_option] = getattr(options, factorio_option).value
|
||||
|
||||
if getattr(multiworld, "silo")[player].value == Options.Silo.option_randomize_recipe:
|
||||
if getattr(options, "silo").value == Options.Silo.option_randomize_recipe:
|
||||
template_data["free_sample_blacklist"]["rocket-silo"] = 1
|
||||
|
||||
if getattr(multiworld, "satellite")[player].value == Options.Satellite.option_randomize_recipe:
|
||||
if getattr(options, "satellite").value == Options.Satellite.option_randomize_recipe:
|
||||
template_data["free_sample_blacklist"]["satellite"] = 1
|
||||
|
||||
template_data["free_sample_blacklist"].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})
|
||||
template_data["free_sample_blacklist"].update({item: 1 for item in options.free_sample_blacklist.value})
|
||||
template_data["free_sample_blacklist"].update({item: 0 for item in options.free_sample_whitelist.value})
|
||||
|
||||
zf_path = os.path.join(output_directory, versioned_mod_name + ".zip")
|
||||
mod = FactorioModFile(zf_path, player=player, player_name=multiworld.player_name[player])
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
from __future__ import annotations
|
||||
import typing
|
||||
import datetime
|
||||
|
||||
from Options import Choice, OptionDict, OptionSet, Option, DefaultOnToggle, Range, DeathLink, Toggle, \
|
||||
StartInventoryPool
|
||||
StartInventoryPool, PerGameCommonOptions
|
||||
from schema import Schema, Optional, And, Or
|
||||
from dataclasses import dataclass
|
||||
|
||||
# schema helpers
|
||||
FloatRange = lambda low, high: And(Or(int, float), lambda f: low <= f <= high)
|
||||
@@ -210,7 +210,7 @@ class RecipeIngredientsOffset(Range):
|
||||
class FactorioStartItems(OptionDict):
|
||||
"""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}
|
||||
default = {"burner-mining-drill": 19, "stone-furnace": 19}
|
||||
|
||||
|
||||
class FactorioFreeSampleBlacklist(OptionSet):
|
||||
@@ -422,50 +422,44 @@ class EnergyLink(Toggle):
|
||||
display_name = "EnergyLink"
|
||||
|
||||
|
||||
factorio_options: typing.Dict[str, type(Option)] = {
|
||||
"max_science_pack": MaxSciencePack,
|
||||
"goal": Goal,
|
||||
"tech_tree_layout": TechTreeLayout,
|
||||
"min_tech_cost": MinTechCost,
|
||||
"max_tech_cost": MaxTechCost,
|
||||
"tech_cost_distribution": TechCostDistribution,
|
||||
"tech_cost_mix": TechCostMix,
|
||||
"ramping_tech_costs": RampingTechCosts,
|
||||
"silo": Silo,
|
||||
"satellite": Satellite,
|
||||
"free_samples": FreeSamples,
|
||||
"tech_tree_information": TechTreeInformation,
|
||||
"starting_items": FactorioStartItems,
|
||||
"free_sample_blacklist": FactorioFreeSampleBlacklist,
|
||||
"free_sample_whitelist": FactorioFreeSampleWhitelist,
|
||||
"recipe_time": RecipeTime,
|
||||
"recipe_ingredients": RecipeIngredients,
|
||||
"recipe_ingredients_offset": RecipeIngredientsOffset,
|
||||
"imported_blueprints": ImportedBlueprint,
|
||||
"world_gen": FactorioWorldGen,
|
||||
"progressive": Progressive,
|
||||
"teleport_traps": TeleportTrapCount,
|
||||
"grenade_traps": GrenadeTrapCount,
|
||||
"cluster_grenade_traps": ClusterGrenadeTrapCount,
|
||||
"artillery_traps": ArtilleryTrapCount,
|
||||
"atomic_rocket_traps": AtomicRocketTrapCount,
|
||||
"attack_traps": AttackTrapCount,
|
||||
"evolution_traps": EvolutionTrapCount,
|
||||
"evolution_trap_increase": EvolutionTrapIncrease,
|
||||
"death_link": DeathLink,
|
||||
"energy_link": EnergyLink,
|
||||
"start_inventory_from_pool": StartInventoryPool,
|
||||
}
|
||||
|
||||
# spoilers below. If you spoil it for yourself, please at least don't spoil it for anyone else.
|
||||
if datetime.datetime.today().month == 4:
|
||||
|
||||
class ChunkShuffle(Toggle):
|
||||
"""Entrance Randomizer."""
|
||||
display_name = "Chunk Shuffle"
|
||||
class ChunkShuffle(Toggle):
|
||||
"""Entrance Randomizer.
|
||||
2023 April Fool's option. Shuffles chunk border transitions.
|
||||
Only valid during the Month of April. Forced off otherwise."""
|
||||
|
||||
|
||||
if datetime.datetime.today().day > 1:
|
||||
ChunkShuffle.__doc__ += """
|
||||
2023 April Fool's option. Shuffles chunk border transitions."""
|
||||
factorio_options["chunk_shuffle"] = ChunkShuffle
|
||||
@dataclass
|
||||
class FactorioOptions(PerGameCommonOptions):
|
||||
max_science_pack: MaxSciencePack
|
||||
goal: Goal
|
||||
tech_tree_layout: TechTreeLayout
|
||||
min_tech_cost: MinTechCost
|
||||
max_tech_cost: MaxTechCost
|
||||
tech_cost_distribution: TechCostDistribution
|
||||
tech_cost_mix: TechCostMix
|
||||
ramping_tech_costs: RampingTechCosts
|
||||
silo: Silo
|
||||
satellite: Satellite
|
||||
free_samples: FreeSamples
|
||||
tech_tree_information: TechTreeInformation
|
||||
starting_items: FactorioStartItems
|
||||
free_sample_blacklist: FactorioFreeSampleBlacklist
|
||||
free_sample_whitelist: FactorioFreeSampleWhitelist
|
||||
recipe_time: RecipeTime
|
||||
recipe_ingredients: RecipeIngredients
|
||||
recipe_ingredients_offset: RecipeIngredientsOffset
|
||||
imported_blueprints: ImportedBlueprint
|
||||
world_gen: FactorioWorldGen
|
||||
progressive: Progressive
|
||||
teleport_traps: TeleportTrapCount
|
||||
grenade_traps: GrenadeTrapCount
|
||||
cluster_grenade_traps: ClusterGrenadeTrapCount
|
||||
artillery_traps: ArtilleryTrapCount
|
||||
atomic_rocket_traps: AtomicRocketTrapCount
|
||||
attack_traps: AttackTrapCount
|
||||
evolution_traps: EvolutionTrapCount
|
||||
evolution_trap_increase: EvolutionTrapIncrease
|
||||
death_link: DeathLink
|
||||
energy_link: EnergyLink
|
||||
start_inventory_from_pool: StartInventoryPool
|
||||
chunk_shuffle: ChunkShuffle
|
||||
|
||||
@@ -20,10 +20,10 @@ def _sorter(location: "FactorioScienceLocation"):
|
||||
|
||||
|
||||
def get_shapes(factorio_world: "Factorio") -> Dict["FactorioScienceLocation", Set["FactorioScienceLocation"]]:
|
||||
options = factorio_world.options
|
||||
world = factorio_world.multiworld
|
||||
player = factorio_world.player
|
||||
prerequisites: Dict["FactorioScienceLocation", Set["FactorioScienceLocation"]] = {}
|
||||
layout = world.tech_tree_layout[player].value
|
||||
layout = options.tech_tree_layout.value
|
||||
locations: List["FactorioScienceLocation"] = sorted(factorio_world.science_locations, key=lambda loc: loc.name)
|
||||
world.random.shuffle(locations)
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ from worlds.LauncherComponents import Component, components, Type, launch_subpro
|
||||
from worlds.generic import Rules
|
||||
from .Locations import location_pools, location_table
|
||||
from .Mod import generate_mod
|
||||
from .Options import factorio_options, MaxSciencePack, Silo, Satellite, TechTreeInformation, Goal, TechCostDistribution
|
||||
from .Options import FactorioOptions, MaxSciencePack, Silo, Satellite, TechTreeInformation, Goal, TechCostDistribution, TechCostMix
|
||||
from .Shapes import get_shapes
|
||||
from .Technologies import base_tech_table, recipe_sources, base_technology_table, \
|
||||
all_ingredient_names, all_product_sources, required_technologies, get_rocket_requirements, \
|
||||
@@ -88,6 +88,9 @@ class Factorio(World):
|
||||
location_pool: typing.List[FactorioScienceLocation]
|
||||
advancement_technologies: typing.Set[str]
|
||||
|
||||
options = FactorioOptions
|
||||
options_dataclass = FactorioOptions
|
||||
|
||||
web = FactorioWeb()
|
||||
|
||||
item_name_to_id = all_items
|
||||
@@ -117,11 +120,11 @@ class Factorio(World):
|
||||
|
||||
def generate_early(self) -> None:
|
||||
# if max < min, then swap max and min
|
||||
if self.multiworld.max_tech_cost[self.player] < self.multiworld.min_tech_cost[self.player]:
|
||||
self.multiworld.min_tech_cost[self.player].value, self.multiworld.max_tech_cost[self.player].value = \
|
||||
self.multiworld.max_tech_cost[self.player].value, self.multiworld.min_tech_cost[self.player].value
|
||||
self.tech_mix = self.multiworld.tech_cost_mix[self.player]
|
||||
self.skip_silo = self.multiworld.silo[self.player].value == Silo.option_spawn
|
||||
if self.options.max_tech_cost < self.options.min_tech_cost:
|
||||
self.options.min_tech_cost.value, self.options.max_tech_cost.value = \
|
||||
self.options.max_tech_cost.value, self.options.min_tech_cost.value
|
||||
self.tech_mix = self.options.tech_cost_mix
|
||||
self.skip_silo = self.options.silo.value == Silo.option_spawn
|
||||
|
||||
def create_regions(self):
|
||||
player = self.player
|
||||
@@ -132,17 +135,17 @@ class Factorio(World):
|
||||
nauvis = Region("Nauvis", player, self.multiworld)
|
||||
|
||||
location_count = len(base_tech_table) - len(useless_technologies) - self.skip_silo + \
|
||||
self.multiworld.evolution_traps[player] + \
|
||||
self.multiworld.attack_traps[player] + \
|
||||
self.multiworld.teleport_traps[player] + \
|
||||
self.multiworld.grenade_traps[player] + \
|
||||
self.multiworld.cluster_grenade_traps[player] + \
|
||||
self.multiworld.atomic_rocket_traps[player] + \
|
||||
self.multiworld.artillery_traps[player]
|
||||
self.options.evolution_traps + \
|
||||
self.options.attack_traps + \
|
||||
self.options.teleport_traps + \
|
||||
self.options.grenade_traps + \
|
||||
self.options.cluster_grenade_traps + \
|
||||
self.options.atomic_rocket_traps + \
|
||||
self.options.artillery_traps
|
||||
|
||||
location_pool = []
|
||||
|
||||
for pack in sorted(self.multiworld.max_science_pack[self.player].get_allowed_packs()):
|
||||
for pack in sorted(self.options.max_science_pack.get_allowed_packs()):
|
||||
location_pool.extend(location_pools[pack])
|
||||
try:
|
||||
location_names = self.multiworld.random.sample(location_pool, location_count)
|
||||
@@ -151,11 +154,11 @@ class Factorio(World):
|
||||
raise Exception("Too many traps for too few locations. Either decrease the trap count, "
|
||||
f"or increase the location count (higher max science pack). (Player {self.player})") from e
|
||||
|
||||
self.science_locations = [FactorioScienceLocation(player, loc_name, self.location_name_to_id[loc_name], nauvis)
|
||||
self.science_locations = [FactorioScienceLocation(player, loc_name, self.location_name_to_id[loc_name], nauvis, self.options.tech_cost_mix)
|
||||
for loc_name in location_names]
|
||||
distribution: TechCostDistribution = self.multiworld.tech_cost_distribution[self.player]
|
||||
min_cost = self.multiworld.min_tech_cost[self.player]
|
||||
max_cost = self.multiworld.max_tech_cost[self.player]
|
||||
distribution: TechCostDistribution = self.options.tech_cost_distribution
|
||||
min_cost = self.options.min_tech_cost
|
||||
max_cost = self.options.max_tech_cost
|
||||
if distribution == distribution.option_even:
|
||||
rand_values = (random.randint(min_cost, max_cost) for _ in self.science_locations)
|
||||
else:
|
||||
@@ -164,7 +167,7 @@ class Factorio(World):
|
||||
distribution.option_high: max_cost}[distribution.value]
|
||||
rand_values = (random.triangular(min_cost, max_cost, mode) for _ in self.science_locations)
|
||||
rand_values = sorted(rand_values)
|
||||
if self.multiworld.ramping_tech_costs[self.player]:
|
||||
if self.options.ramping_tech_costs:
|
||||
def sorter(loc: FactorioScienceLocation):
|
||||
return loc.complexity, loc.rel_cost
|
||||
else:
|
||||
@@ -179,7 +182,7 @@ class Factorio(World):
|
||||
event = FactorioItem("Victory", ItemClassification.progression, None, player)
|
||||
location.place_locked_item(event)
|
||||
|
||||
for ingredient in sorted(self.multiworld.max_science_pack[self.player].get_allowed_packs()):
|
||||
for ingredient in sorted(self.options.max_science_pack.get_allowed_packs()):
|
||||
location = FactorioLocation(player, f"Automate {ingredient}", None, nauvis)
|
||||
nauvis.locations.append(location)
|
||||
event = FactorioItem(f"Automated {ingredient}", ItemClassification.progression, None, player)
|
||||
@@ -195,10 +198,10 @@ class Factorio(World):
|
||||
traps = ("Evolution", "Attack", "Teleport", "Grenade", "Cluster Grenade", "Artillery", "Atomic Rocket")
|
||||
for trap_name in traps:
|
||||
self.multiworld.itempool.extend(self.create_item(f"{trap_name} Trap") for _ in
|
||||
range(getattr(self.multiworld,
|
||||
f"{trap_name.lower().replace(' ', '_')}_traps")[player]))
|
||||
range(getattr(self.options,
|
||||
f"{trap_name.lower().replace(' ', '_')}_traps")))
|
||||
|
||||
want_progressives = collections.defaultdict(lambda: self.multiworld.progressive[player].
|
||||
want_progressives = collections.defaultdict(lambda: self.options.progressive.
|
||||
want_progressives(self.multiworld.random))
|
||||
|
||||
cost_sorted_locations = sorted(self.science_locations, key=lambda location: location.name)
|
||||
@@ -206,7 +209,7 @@ class Factorio(World):
|
||||
"logistics": 1,
|
||||
"rocket-silo": -1}
|
||||
loc: FactorioScienceLocation
|
||||
if self.multiworld.tech_tree_information[player] == TechTreeInformation.option_full:
|
||||
if self.options.tech_tree_information == TechTreeInformation.option_full:
|
||||
# mark all locations as pre-hinted
|
||||
for loc in self.science_locations:
|
||||
loc.revealed = True
|
||||
@@ -237,10 +240,10 @@ class Factorio(World):
|
||||
player = self.player
|
||||
shapes = get_shapes(self)
|
||||
|
||||
for ingredient in self.multiworld.max_science_pack[self.player].get_allowed_packs():
|
||||
for ingredient in self.options.max_science_pack.get_allowed_packs():
|
||||
location = world.get_location(f"Automate {ingredient}", player)
|
||||
|
||||
if self.multiworld.recipe_ingredients[self.player]:
|
||||
if self.options.recipe_ingredients:
|
||||
custom_recipe = self.custom_recipes[ingredient]
|
||||
|
||||
location.access_rule = lambda state, ingredient=ingredient, custom_recipe=custom_recipe: \
|
||||
@@ -261,16 +264,16 @@ class Factorio(World):
|
||||
prerequisites: all(state.can_reach(loc) for loc in locations))
|
||||
|
||||
silo_recipe = None
|
||||
if self.multiworld.silo[self.player] == Silo.option_spawn:
|
||||
if self.options.silo == Silo.option_spawn:
|
||||
silo_recipe = self.custom_recipes["rocket-silo"] if "rocket-silo" in self.custom_recipes \
|
||||
else next(iter(all_product_sources.get("rocket-silo")))
|
||||
part_recipe = self.custom_recipes["rocket-part"]
|
||||
satellite_recipe = None
|
||||
if self.multiworld.goal[self.player] == Goal.option_satellite:
|
||||
if self.options.goal == Goal.option_satellite:
|
||||
satellite_recipe = self.custom_recipes["satellite"] if "satellite" in self.custom_recipes \
|
||||
else next(iter(all_product_sources.get("satellite")))
|
||||
victory_tech_names = get_rocket_requirements(silo_recipe, part_recipe, satellite_recipe)
|
||||
if self.multiworld.silo[self.player] != Silo.option_spawn:
|
||||
if self.options.silo != Silo.option_spawn:
|
||||
victory_tech_names.add("rocket-silo")
|
||||
world.get_location("Rocket Launch", player).access_rule = lambda state: all(state.has(technology, player)
|
||||
for technology in
|
||||
@@ -279,12 +282,12 @@ class Factorio(World):
|
||||
world.completion_condition[player] = lambda state: state.has('Victory', player)
|
||||
|
||||
def generate_basic(self):
|
||||
map_basic_settings = self.multiworld.world_gen[self.player].value["basic"]
|
||||
map_basic_settings = self.options.world_gen.value["basic"]
|
||||
if map_basic_settings.get("seed", None) is None: # allow seed 0
|
||||
# 32 bit uint
|
||||
map_basic_settings["seed"] = self.multiworld.per_slot_randoms[self.player].randint(0, 2 ** 32 - 1)
|
||||
|
||||
start_location_hints: typing.Set[str] = self.multiworld.start_location_hints[self.player].value
|
||||
start_location_hints: typing.Set[str] = self.options.start_location_hints.value
|
||||
|
||||
for loc in self.science_locations:
|
||||
# show start_location_hints ingame
|
||||
@@ -308,8 +311,6 @@ class Factorio(World):
|
||||
|
||||
return super(Factorio, self).collect_item(state, item, remove)
|
||||
|
||||
option_definitions = factorio_options
|
||||
|
||||
@classmethod
|
||||
def stage_write_spoiler(cls, world, spoiler_handle):
|
||||
factorio_players = world.get_game_players(cls.game)
|
||||
@@ -437,25 +438,25 @@ class Factorio(World):
|
||||
|
||||
def set_custom_technologies(self):
|
||||
custom_technologies = {}
|
||||
allowed_packs = self.multiworld.max_science_pack[self.player].get_allowed_packs()
|
||||
allowed_packs = self.options.max_science_pack.get_allowed_packs()
|
||||
for technology_name, technology in base_technology_table.items():
|
||||
custom_technologies[technology_name] = technology.get_custom(self.multiworld, allowed_packs, self.player)
|
||||
return custom_technologies
|
||||
|
||||
def set_custom_recipes(self):
|
||||
ingredients_offset = self.multiworld.recipe_ingredients_offset[self.player]
|
||||
ingredients_offset = self.options.recipe_ingredients_offset
|
||||
original_rocket_part = recipes["rocket-part"]
|
||||
science_pack_pools = get_science_pack_pools()
|
||||
valid_pool = sorted(science_pack_pools[self.multiworld.max_science_pack[self.player].get_max_pack()] & valid_ingredients)
|
||||
valid_pool = sorted(science_pack_pools[self.options.max_science_pack.get_max_pack()] & valid_ingredients)
|
||||
self.multiworld.random.shuffle(valid_pool)
|
||||
self.custom_recipes = {"rocket-part": Recipe("rocket-part", original_rocket_part.category,
|
||||
{valid_pool[x]: 10 for x in range(3 + ingredients_offset)},
|
||||
original_rocket_part.products,
|
||||
original_rocket_part.energy)}
|
||||
|
||||
if self.multiworld.recipe_ingredients[self.player]:
|
||||
if self.options.recipe_ingredients:
|
||||
valid_pool = []
|
||||
for pack in self.multiworld.max_science_pack[self.player].get_ordered_science_packs():
|
||||
for pack in self.options.max_science_pack.get_ordered_science_packs():
|
||||
valid_pool += sorted(science_pack_pools[pack])
|
||||
self.multiworld.random.shuffle(valid_pool)
|
||||
if pack in recipes: # skips over space science pack
|
||||
@@ -463,23 +464,23 @@ class Factorio(World):
|
||||
ingredients_offset)
|
||||
self.custom_recipes[pack] = new_recipe
|
||||
|
||||
if self.multiworld.silo[self.player].value == Silo.option_randomize_recipe \
|
||||
or self.multiworld.satellite[self.player].value == Satellite.option_randomize_recipe:
|
||||
if self.options.silo.value == Silo.option_randomize_recipe \
|
||||
or self.options.satellite.value == Satellite.option_randomize_recipe:
|
||||
valid_pool = set()
|
||||
for pack in sorted(self.multiworld.max_science_pack[self.player].get_allowed_packs()):
|
||||
for pack in sorted(self.options.max_science_pack.get_allowed_packs()):
|
||||
valid_pool |= science_pack_pools[pack]
|
||||
|
||||
if self.multiworld.silo[self.player].value == Silo.option_randomize_recipe:
|
||||
if self.options.silo.value == Silo.option_randomize_recipe:
|
||||
new_recipe = self.make_balanced_recipe(
|
||||
recipes["rocket-silo"], valid_pool,
|
||||
factor=(self.multiworld.max_science_pack[self.player].value + 1) / 7,
|
||||
factor=(self.options.max_science_pack.value + 1) / 7,
|
||||
ingredients_offset=ingredients_offset)
|
||||
self.custom_recipes["rocket-silo"] = new_recipe
|
||||
|
||||
if self.multiworld.satellite[self.player].value == Satellite.option_randomize_recipe:
|
||||
if self.options.satellite.value == Satellite.option_randomize_recipe:
|
||||
new_recipe = self.make_balanced_recipe(
|
||||
recipes["satellite"], valid_pool,
|
||||
factor=(self.multiworld.max_science_pack[self.player].value + 1) / 7,
|
||||
factor=(self.options.max_science_pack.value + 1) / 7,
|
||||
ingredients_offset=ingredients_offset)
|
||||
self.custom_recipes["satellite"] = new_recipe
|
||||
bridge = "ap-energy-bridge"
|
||||
@@ -487,16 +488,16 @@ class Factorio(World):
|
||||
Recipe(bridge, "crafting", {"replace_1": 1, "replace_2": 1, "replace_3": 1,
|
||||
"replace_4": 1, "replace_5": 1, "replace_6": 1},
|
||||
{bridge: 1}, 10),
|
||||
sorted(science_pack_pools[self.multiworld.max_science_pack[self.player].get_ordered_science_packs()[0]]),
|
||||
sorted(science_pack_pools[self.options.max_science_pack.get_ordered_science_packs()[0]]),
|
||||
ingredients_offset=ingredients_offset)
|
||||
for ingredient_name in new_recipe.ingredients:
|
||||
new_recipe.ingredients[ingredient_name] = self.multiworld.random.randint(50, 500)
|
||||
self.custom_recipes[bridge] = new_recipe
|
||||
|
||||
needed_recipes = self.multiworld.max_science_pack[self.player].get_allowed_packs() | {"rocket-part"}
|
||||
if self.multiworld.silo[self.player] != Silo.option_spawn:
|
||||
needed_recipes = self.options.max_science_pack.get_allowed_packs() | {"rocket-part"}
|
||||
if self.options.silo != Silo.option_spawn:
|
||||
needed_recipes |= {"rocket-silo"}
|
||||
if self.multiworld.goal[self.player].value == Goal.option_satellite:
|
||||
if self.options.goal.value == Goal.option_satellite:
|
||||
needed_recipes |= {"satellite"}
|
||||
|
||||
for recipe in needed_recipes:
|
||||
@@ -538,7 +539,7 @@ class FactorioScienceLocation(FactorioLocation):
|
||||
ingredients: typing.Dict[str, int]
|
||||
count: int = 0
|
||||
|
||||
def __init__(self, player: int, name: str, address: int, parent: Region):
|
||||
def __init__(self, player: int, name: str, address: int, parent: Region, tech_cost_mix: TechCostMix):
|
||||
super(FactorioScienceLocation, self).__init__(player, name, address, parent)
|
||||
# "AP-{Complexity}-{Cost}"
|
||||
self.complexity = int(self.name[3]) - 1
|
||||
@@ -546,7 +547,7 @@ class FactorioScienceLocation(FactorioLocation):
|
||||
|
||||
self.ingredients = {Factorio.ordered_science_packs[self.complexity]: 1}
|
||||
for complexity in range(self.complexity):
|
||||
if parent.multiworld.tech_cost_mix[self.player] > parent.multiworld.random.randint(0, 99):
|
||||
if tech_cost_mix > parent.multiworld.random.randint(0, 99):
|
||||
self.ingredients[Factorio.ordered_science_packs[complexity]] = 1
|
||||
|
||||
@property
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -71,7 +71,7 @@ class FFMQClient(SNIClient):
|
||||
received = await snes_read(ctx, RECEIVED_DATA[0], RECEIVED_DATA[1])
|
||||
data = await snes_read(ctx, READ_DATA_START, READ_DATA_END - READ_DATA_START)
|
||||
check_2 = await snes_read(ctx, 0xF53749, 1)
|
||||
if check_1 in (b'\x00', b'\x55') or check_2 in (b'\x00', b'\x55'):
|
||||
if check_1 == b'\x00' or check_2 == b'\x00':
|
||||
return
|
||||
|
||||
def get_range(data_range):
|
||||
|
||||
@@ -223,6 +223,11 @@ for item, data in item_table.items():
|
||||
def create_items(self) -> None:
|
||||
items = []
|
||||
starting_weapon = self.multiworld.starting_weapon[self.player].current_key.title().replace("_", " ")
|
||||
if self.multiworld.progressive_gear[self.player]:
|
||||
for item_group in prog_map:
|
||||
if starting_weapon in self.item_name_groups[item_group]:
|
||||
starting_weapon = prog_map[item_group]
|
||||
break
|
||||
self.multiworld.push_precollected(self.create_item(starting_weapon))
|
||||
self.multiworld.push_precollected(self.create_item("Steel Armor"))
|
||||
if self.multiworld.sky_coin_mode[self.player] == "start_with":
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import collections
|
||||
import logging
|
||||
import typing
|
||||
|
||||
from BaseClasses import LocationProgressType, MultiWorld, Location, Region, Entrance
|
||||
@@ -16,9 +15,9 @@ else:
|
||||
|
||||
def locality_needed(world: MultiWorld) -> bool:
|
||||
for player in world.player_ids:
|
||||
if world.local_items[player].value:
|
||||
if world.worlds[player].options.local_items.value:
|
||||
return True
|
||||
if world.non_local_items[player].value:
|
||||
if world.worlds[player].options.non_local_items.value:
|
||||
return True
|
||||
|
||||
# Group
|
||||
@@ -41,12 +40,12 @@ def locality_rules(world: MultiWorld):
|
||||
forbid_data[sender][receiver].update(items)
|
||||
|
||||
for receiving_player in world.player_ids:
|
||||
local_items: typing.Set[str] = world.local_items[receiving_player].value
|
||||
local_items: typing.Set[str] = world.worlds[receiving_player].options.local_items.value
|
||||
if local_items:
|
||||
for sending_player in world.player_ids:
|
||||
if receiving_player != sending_player:
|
||||
forbid(sending_player, receiving_player, local_items)
|
||||
non_local_items: typing.Set[str] = world.non_local_items[receiving_player].value
|
||||
non_local_items: typing.Set[str] = world.worlds[receiving_player].options.non_local_items.value
|
||||
if non_local_items:
|
||||
forbid(receiving_player, receiving_player, non_local_items)
|
||||
|
||||
@@ -82,18 +81,15 @@ def locality_rules(world: MultiWorld):
|
||||
i.name not in sending_blockers[i.player] and old_rule(i)
|
||||
|
||||
|
||||
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):
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -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),
|
||||
|
||||
@@ -80,6 +80,11 @@ class KH2Context(CommonContext):
|
||||
},
|
||||
},
|
||||
}
|
||||
self.front_of_inventory = {
|
||||
"Sora": 0x2546,
|
||||
"Donald": 0x2658,
|
||||
"Goofy": 0x276C,
|
||||
}
|
||||
self.kh2seedname = None
|
||||
self.kh2slotdata = None
|
||||
self.itemamount = {}
|
||||
@@ -164,14 +169,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 +219,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 +476,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 +507,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 +518,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 +615,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 +673,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 +740,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)
|
||||
@@ -821,8 +762,7 @@ class KH2Context(CommonContext):
|
||||
|
||||
def finishedGame(ctx: KH2Context, message):
|
||||
if ctx.kh2slotdata['FinalXemnas'] == 1:
|
||||
if not ctx.final_xemnas and ctx.kh2_read_byte(ctx.Save + all_world_locations[LocationName.FinalXemnas].addrObtained) \
|
||||
& 0x1 << all_world_locations[LocationName.FinalXemnas].bitIndex > 0:
|
||||
if not ctx.final_xemnas and ctx.kh2_loc_name_to_id[LocationName.FinalXemnas] in ctx.locations_checked:
|
||||
ctx.final_xemnas = True
|
||||
# three proofs
|
||||
if ctx.kh2slotdata['Goal'] == 0:
|
||||
|
||||
@@ -2,7 +2,22 @@ import typing
|
||||
|
||||
from BaseClasses import Item
|
||||
from .Names import ItemName
|
||||
from .Subclasses import ItemData
|
||||
|
||||
|
||||
class KH2Item(Item):
|
||||
game: str = "Kingdom Hearts 2"
|
||||
|
||||
|
||||
class ItemData(typing.NamedTuple):
|
||||
quantity: int = 0
|
||||
kh2id: int = 0
|
||||
# Save+ mem addr
|
||||
memaddr: int = 0
|
||||
# some items have bitmasks. if bitmask>0 bitor to give item else
|
||||
bitmask: int = 0
|
||||
# if ability then
|
||||
ability: bool = False
|
||||
|
||||
|
||||
# 0x130000
|
||||
Reports_Table = {
|
||||
@@ -194,7 +209,7 @@ Armor_Table = {
|
||||
ItemName.GrandRibbon: ItemData(1, 157, 0x35D4),
|
||||
}
|
||||
Usefull_Table = {
|
||||
ItemName.MickeyMunnyPouch: ItemData(1, 535, 0x3695), # 5000 munny per
|
||||
ItemName.MickeyMunnyPouch: ItemData(1, 535, 0x3695), # 5000 munny per
|
||||
ItemName.OletteMunnyPouch: ItemData(2, 362, 0x363C), # 2500 munny per
|
||||
ItemName.HadesCupTrophy: ItemData(1, 537, 0x3696),
|
||||
ItemName.UnknownDisk: ItemData(1, 462, 0x365F),
|
||||
@@ -334,7 +349,7 @@ GoofyAbility_Table = {
|
||||
|
||||
Wincon_Table = {
|
||||
ItemName.LuckyEmblem: ItemData(kh2id=367, memaddr=0x3641), # letter item
|
||||
# ItemName.Victory: ItemData(kh2id=263, memaddr=0x111),
|
||||
ItemName.Victory: ItemData(kh2id=263, memaddr=0x111),
|
||||
ItemName.Bounty: ItemData(kh2id=461, memaddr=0x365E), # Dummy 14
|
||||
# ItemName.UniversalKey:ItemData(,365,0x363F,0)#Tournament Poster
|
||||
}
|
||||
|
||||
@@ -1,9 +1,19 @@
|
||||
import typing
|
||||
|
||||
from BaseClasses import Location
|
||||
from .Names import LocationName, ItemName, RegionName
|
||||
from .Subclasses import LocationData
|
||||
from .Regions import KH2REGIONS
|
||||
from .Names import LocationName, ItemName
|
||||
|
||||
|
||||
class KH2Location(Location):
|
||||
game: str = "Kingdom Hearts 2"
|
||||
|
||||
|
||||
class LocationData(typing.NamedTuple):
|
||||
locid: int
|
||||
yml: str
|
||||
charName: str = "Sora"
|
||||
charNumber: int = 1
|
||||
|
||||
|
||||
# data's addrcheck sys3 addr obtained roomid bit index is eventid
|
||||
LoD_Checks = {
|
||||
@@ -531,7 +541,7 @@ TWTNW_Checks = {
|
||||
LocationName.Xemnas1: LocationData(26, "Double Get Bonus"),
|
||||
LocationName.Xemnas1GetBonus: LocationData(26, "Second Get Bonus"),
|
||||
LocationName.Xemnas1SecretAnsemReport13: LocationData(537, "Chest"),
|
||||
# LocationName.FinalXemnas: LocationData(71, "Get Bonus"),
|
||||
LocationName.FinalXemnas: LocationData(71, "Get Bonus"),
|
||||
LocationName.XemnasDataPowerBoost: LocationData(554, "Chest"),
|
||||
}
|
||||
|
||||
@@ -796,75 +806,74 @@ Atlantica_Checks = {
|
||||
}
|
||||
|
||||
event_location_to_item = {
|
||||
LocationName.HostileProgramEventLocation: ItemName.HostileProgramEvent,
|
||||
LocationName.McpEventLocation: ItemName.McpEvent,
|
||||
LocationName.HostileProgramEventLocation: ItemName.HostileProgramEvent,
|
||||
LocationName.McpEventLocation: ItemName.McpEvent,
|
||||
# LocationName.ASLarxeneEventLocation: ItemName.ASLarxeneEvent,
|
||||
LocationName.DataLarxeneEventLocation: ItemName.DataLarxeneEvent,
|
||||
LocationName.BarbosaEventLocation: ItemName.BarbosaEvent,
|
||||
LocationName.GrimReaper1EventLocation: ItemName.GrimReaper1Event,
|
||||
LocationName.GrimReaper2EventLocation: ItemName.GrimReaper2Event,
|
||||
LocationName.DataLuxordEventLocation: ItemName.DataLuxordEvent,
|
||||
LocationName.DataAxelEventLocation: ItemName.DataAxelEvent,
|
||||
LocationName.CerberusEventLocation: ItemName.CerberusEvent,
|
||||
LocationName.OlympusPeteEventLocation: ItemName.OlympusPeteEvent,
|
||||
LocationName.HydraEventLocation: ItemName.HydraEvent,
|
||||
LocationName.DataLarxeneEventLocation: ItemName.DataLarxeneEvent,
|
||||
LocationName.BarbosaEventLocation: ItemName.BarbosaEvent,
|
||||
LocationName.GrimReaper1EventLocation: ItemName.GrimReaper1Event,
|
||||
LocationName.GrimReaper2EventLocation: ItemName.GrimReaper2Event,
|
||||
LocationName.DataLuxordEventLocation: ItemName.DataLuxordEvent,
|
||||
LocationName.DataAxelEventLocation: ItemName.DataAxelEvent,
|
||||
LocationName.CerberusEventLocation: ItemName.CerberusEvent,
|
||||
LocationName.OlympusPeteEventLocation: ItemName.OlympusPeteEvent,
|
||||
LocationName.HydraEventLocation: ItemName.HydraEvent,
|
||||
LocationName.OcPainAndPanicCupEventLocation: ItemName.OcPainAndPanicCupEvent,
|
||||
LocationName.OcCerberusCupEventLocation: ItemName.OcCerberusCupEvent,
|
||||
LocationName.HadesEventLocation: ItemName.HadesEvent,
|
||||
LocationName.OcCerberusCupEventLocation: ItemName.OcCerberusCupEvent,
|
||||
LocationName.HadesEventLocation: ItemName.HadesEvent,
|
||||
# LocationName.ASZexionEventLocation: ItemName.ASZexionEvent,
|
||||
LocationName.DataZexionEventLocation: ItemName.DataZexionEvent,
|
||||
LocationName.Oc2TitanCupEventLocation: ItemName.Oc2TitanCupEvent,
|
||||
LocationName.Oc2GofCupEventLocation: ItemName.Oc2GofCupEvent,
|
||||
LocationName.DataZexionEventLocation: ItemName.DataZexionEvent,
|
||||
LocationName.Oc2TitanCupEventLocation: ItemName.Oc2TitanCupEvent,
|
||||
LocationName.Oc2GofCupEventLocation: ItemName.Oc2GofCupEvent,
|
||||
# LocationName.Oc2CupsEventLocation: ItemName.Oc2CupsEventLocation,
|
||||
LocationName.HadesCupEventLocations: ItemName.HadesCupEvents,
|
||||
LocationName.PrisonKeeperEventLocation: ItemName.PrisonKeeperEvent,
|
||||
LocationName.OogieBoogieEventLocation: ItemName.OogieBoogieEvent,
|
||||
LocationName.ExperimentEventLocation: ItemName.ExperimentEvent,
|
||||
LocationName.HadesCupEventLocations: ItemName.HadesCupEvents,
|
||||
LocationName.PrisonKeeperEventLocation: ItemName.PrisonKeeperEvent,
|
||||
LocationName.OogieBoogieEventLocation: ItemName.OogieBoogieEvent,
|
||||
LocationName.ExperimentEventLocation: ItemName.ExperimentEvent,
|
||||
# LocationName.ASVexenEventLocation: ItemName.ASVexenEvent,
|
||||
LocationName.DataVexenEventLocation: ItemName.DataVexenEvent,
|
||||
LocationName.ShanYuEventLocation: ItemName.ShanYuEvent,
|
||||
LocationName.AnsemRikuEventLocation: ItemName.AnsemRikuEvent,
|
||||
LocationName.StormRiderEventLocation: ItemName.StormRiderEvent,
|
||||
LocationName.DataXigbarEventLocation: ItemName.DataXigbarEvent,
|
||||
LocationName.RoxasEventLocation: ItemName.RoxasEvent,
|
||||
LocationName.XigbarEventLocation: ItemName.XigbarEvent,
|
||||
LocationName.LuxordEventLocation: ItemName.LuxordEvent,
|
||||
LocationName.SaixEventLocation: ItemName.SaixEvent,
|
||||
LocationName.XemnasEventLocation: ItemName.XemnasEvent,
|
||||
LocationName.ArmoredXemnasEventLocation: ItemName.ArmoredXemnasEvent,
|
||||
LocationName.ArmoredXemnas2EventLocation: ItemName.ArmoredXemnas2Event,
|
||||
LocationName.DataVexenEventLocation: ItemName.DataVexenEvent,
|
||||
LocationName.ShanYuEventLocation: ItemName.ShanYuEvent,
|
||||
LocationName.AnsemRikuEventLocation: ItemName.AnsemRikuEvent,
|
||||
LocationName.StormRiderEventLocation: ItemName.StormRiderEvent,
|
||||
LocationName.DataXigbarEventLocation: ItemName.DataXigbarEvent,
|
||||
LocationName.RoxasEventLocation: ItemName.RoxasEvent,
|
||||
LocationName.XigbarEventLocation: ItemName.XigbarEvent,
|
||||
LocationName.LuxordEventLocation: ItemName.LuxordEvent,
|
||||
LocationName.SaixEventLocation: ItemName.SaixEvent,
|
||||
LocationName.XemnasEventLocation: ItemName.XemnasEvent,
|
||||
LocationName.ArmoredXemnasEventLocation: ItemName.ArmoredXemnasEvent,
|
||||
LocationName.ArmoredXemnas2EventLocation: ItemName.ArmoredXemnas2Event,
|
||||
# LocationName.FinalXemnasEventLocation: ItemName.FinalXemnasEvent,
|
||||
LocationName.DataXemnasEventLocation: ItemName.DataXemnasEvent,
|
||||
LocationName.ThresholderEventLocation: ItemName.ThresholderEvent,
|
||||
LocationName.BeastEventLocation: ItemName.BeastEvent,
|
||||
LocationName.DarkThornEventLocation: ItemName.DarkThornEvent,
|
||||
LocationName.XaldinEventLocation: ItemName.XaldinEvent,
|
||||
LocationName.DataXaldinEventLocation: ItemName.DataXaldinEvent,
|
||||
LocationName.TwinLordsEventLocation: ItemName.TwinLordsEvent,
|
||||
LocationName.GenieJafarEventLocation: ItemName.GenieJafarEvent,
|
||||
LocationName.DataXemnasEventLocation: ItemName.DataXemnasEvent,
|
||||
LocationName.ThresholderEventLocation: ItemName.ThresholderEvent,
|
||||
LocationName.BeastEventLocation: ItemName.BeastEvent,
|
||||
LocationName.DarkThornEventLocation: ItemName.DarkThornEvent,
|
||||
LocationName.XaldinEventLocation: ItemName.XaldinEvent,
|
||||
LocationName.DataXaldinEventLocation: ItemName.DataXaldinEvent,
|
||||
LocationName.TwinLordsEventLocation: ItemName.TwinLordsEvent,
|
||||
LocationName.GenieJafarEventLocation: ItemName.GenieJafarEvent,
|
||||
# LocationName.ASLexaeusEventLocation: ItemName.ASLexaeusEvent,
|
||||
LocationName.DataLexaeusEventLocation: ItemName.DataLexaeusEvent,
|
||||
LocationName.ScarEventLocation: ItemName.ScarEvent,
|
||||
LocationName.GroundShakerEventLocation: ItemName.GroundShakerEvent,
|
||||
LocationName.DataSaixEventLocation: ItemName.DataSaixEvent,
|
||||
LocationName.HBDemyxEventLocation: ItemName.HBDemyxEvent,
|
||||
LocationName.ThousandHeartlessEventLocation: ItemName.ThousandHeartlessEvent,
|
||||
LocationName.Mushroom13EventLocation: ItemName.Mushroom13Event,
|
||||
LocationName.SephiEventLocation: ItemName.SephiEvent,
|
||||
LocationName.DataDemyxEventLocation: ItemName.DataDemyxEvent,
|
||||
LocationName.CorFirstFightEventLocation: ItemName.CorFirstFightEvent,
|
||||
LocationName.CorSecondFightEventLocation: ItemName.CorSecondFightEvent,
|
||||
LocationName.TransportEventLocation: ItemName.TransportEvent,
|
||||
LocationName.OldPeteEventLocation: ItemName.OldPeteEvent,
|
||||
LocationName.FuturePeteEventLocation: ItemName.FuturePeteEvent,
|
||||
LocationName.DataLexaeusEventLocation: ItemName.DataLexaeusEvent,
|
||||
LocationName.ScarEventLocation: ItemName.ScarEvent,
|
||||
LocationName.GroundShakerEventLocation: ItemName.GroundShakerEvent,
|
||||
LocationName.DataSaixEventLocation: ItemName.DataSaixEvent,
|
||||
LocationName.HBDemyxEventLocation: ItemName.HBDemyxEvent,
|
||||
LocationName.ThousandHeartlessEventLocation: ItemName.ThousandHeartlessEvent,
|
||||
LocationName.Mushroom13EventLocation: ItemName.Mushroom13Event,
|
||||
LocationName.SephiEventLocation: ItemName.SephiEvent,
|
||||
LocationName.DataDemyxEventLocation: ItemName.DataDemyxEvent,
|
||||
LocationName.CorFirstFightEventLocation: ItemName.CorFirstFightEvent,
|
||||
LocationName.CorSecondFightEventLocation: ItemName.CorSecondFightEvent,
|
||||
LocationName.TransportEventLocation: ItemName.TransportEvent,
|
||||
LocationName.OldPeteEventLocation: ItemName.OldPeteEvent,
|
||||
LocationName.FuturePeteEventLocation: ItemName.FuturePeteEvent,
|
||||
# LocationName.ASMarluxiaEventLocation: ItemName.ASMarluxiaEvent,
|
||||
LocationName.DataMarluxiaEventLocation: ItemName.DataMarluxiaEvent,
|
||||
LocationName.TerraEventLocation: ItemName.TerraEvent,
|
||||
LocationName.TwilightThornEventLocation: ItemName.TwilightThornEvent,
|
||||
LocationName.Axel1EventLocation: ItemName.Axel1Event,
|
||||
LocationName.Axel2EventLocation: ItemName.Axel2Event,
|
||||
LocationName.DataRoxasEventLocation: ItemName.DataRoxasEvent,
|
||||
LocationName.FinalXemnasEventLocation: ItemName.Victory,
|
||||
LocationName.DataMarluxiaEventLocation: ItemName.DataMarluxiaEvent,
|
||||
LocationName.TerraEventLocation: ItemName.TerraEvent,
|
||||
LocationName.TwilightThornEventLocation: ItemName.TwilightThornEvent,
|
||||
LocationName.Axel1EventLocation: ItemName.Axel1Event,
|
||||
LocationName.Axel2EventLocation: ItemName.Axel2Event,
|
||||
LocationName.DataRoxasEventLocation: ItemName.DataRoxasEvent,
|
||||
}
|
||||
all_weapon_slot = {
|
||||
LocationName.FAKESlot,
|
||||
@@ -1352,9 +1361,3 @@ exclusion_table = {
|
||||
location for location, data in all_locations.items() if location not in event_location_to_item.keys() and location not in popups_set and location != LocationName.StationofSerenityPotion and data.yml == "Chest"
|
||||
}
|
||||
}
|
||||
|
||||
location_groups: typing.Dict[str, list]
|
||||
location_groups = {
|
||||
Region_Name: [loc for loc in Region_Locs if "Event" not in loc]
|
||||
for Region_Name, Region_Locs in KH2REGIONS.items() if Region_Locs
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ from dataclasses import dataclass
|
||||
|
||||
from Options import Choice, Range, Toggle, ItemDict, PerGameCommonOptions, StartInventoryPool
|
||||
|
||||
from . import default_itempool_option
|
||||
from worlds.kh2 import default_itempool_option
|
||||
|
||||
|
||||
class SoraEXP(Range):
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import typing
|
||||
|
||||
from BaseClasses import MultiWorld, Region
|
||||
from . import Locations
|
||||
|
||||
from .Subclasses import KH2Location
|
||||
from .Names import LocationName, RegionName
|
||||
from .Items import Events_Table
|
||||
from .Locations import KH2Location, event_location_to_item
|
||||
from . import LocationName, RegionName, Events_Table
|
||||
|
||||
KH2REGIONS: typing.Dict[str, typing.List[str]] = {
|
||||
"Menu": [],
|
||||
@@ -790,7 +788,7 @@ KH2REGIONS: typing.Dict[str, typing.List[str]] = {
|
||||
LocationName.ArmoredXemnas2EventLocation
|
||||
],
|
||||
RegionName.FinalXemnas: [
|
||||
LocationName.FinalXemnasEventLocation
|
||||
LocationName.FinalXemnas
|
||||
],
|
||||
RegionName.DataXemnas: [
|
||||
LocationName.XemnasDataPowerBoost,
|
||||
@@ -1022,8 +1020,7 @@ 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
|
||||
|
||||
for location, item in Locations.event_location_to_item.items():
|
||||
for location, item in event_location_to_item.items():
|
||||
multiworld.get_location(location, player).place_locked_item(
|
||||
multiworld.worlds[player].create_event_item(item))
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from typing import Dict, Callable, TYPE_CHECKING
|
||||
|
||||
from BaseClasses import CollectionState
|
||||
from .Items import exclusion_item_table, visit_locking_dict, DonaldAbility_Table, GoofyAbility_Table, SupportAbility_Table
|
||||
from .Items import exclusion_item_table, visit_locking_dict, DonaldAbility_Table, GoofyAbility_Table
|
||||
from .Locations import exclusion_table, popups_set, Goofy_Checks, Donald_Checks
|
||||
from .Names import LocationName, ItemName, RegionName
|
||||
from worlds.generic.Rules import add_rule, forbid_items, add_item_rule
|
||||
@@ -83,8 +83,6 @@ class KH2Rules:
|
||||
return state.has(ItemName.TornPages, self.player, amount)
|
||||
|
||||
def level_locking_unlock(self, state: CollectionState, amount):
|
||||
if self.world.options.Promise_Charm and state.has(ItemName.PromiseCharm, self.player):
|
||||
return True
|
||||
return amount <= sum([state.count(item_name, self.player) for item_name in visit_locking_dict["2VisitLocking"]])
|
||||
|
||||
def summon_levels_unlocked(self, state: CollectionState, amount) -> bool:
|
||||
@@ -226,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) and self.kh2_has_all([ItemName.FireElement,ItemName.BlizzardElement,ItemName.ThunderElement],state),
|
||||
|
||||
RegionName.Bc: lambda state: self.bc_unlocked(state, 1),
|
||||
RegionName.Bc2: lambda state: self.bc_unlocked(state, 2),
|
||||
@@ -268,11 +266,10 @@ class KH2WorldRules(KH2Rules):
|
||||
add_item_rule(location, lambda item: item.player == self.player and item.name in GoofyAbility_Table.keys())
|
||||
elif location.name in Donald_Checks:
|
||||
add_item_rule(location, lambda item: item.player == self.player and item.name in DonaldAbility_Table.keys())
|
||||
else:
|
||||
add_item_rule(location, lambda item: item.player == self.player and item.name in SupportAbility_Table.keys())
|
||||
|
||||
def set_kh2_goal(self):
|
||||
final_xemnas_location = self.multiworld.get_location(LocationName.FinalXemnasEventLocation, self.player)
|
||||
|
||||
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)
|
||||
if self.multiworld.FinalXemnas[self.player]:
|
||||
@@ -294,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:
|
||||
@@ -421,7 +418,7 @@ class KH2FightRules(KH2Rules):
|
||||
RegionName.DataLexaeus: lambda state: self.get_data_lexaeus_rules(state),
|
||||
RegionName.OldPete: lambda state: self.get_old_pete_rules(),
|
||||
RegionName.FuturePete: lambda state: self.get_future_pete_rules(state),
|
||||
RegionName.Terra: lambda state: self.get_terra_rules(state) and state.has(ItemName.ProofofConnection, self.player),
|
||||
RegionName.Terra: lambda state: self.get_terra_rules(state),
|
||||
RegionName.DataMarluxia: lambda state: self.get_data_marluxia_rules(state),
|
||||
RegionName.Barbosa: lambda state: self.get_barbosa_rules(state),
|
||||
RegionName.GrimReaper1: lambda state: self.get_grim_reaper1_rules(),
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
import typing
|
||||
|
||||
from BaseClasses import Location, Item
|
||||
|
||||
|
||||
class KH2Location(Location):
|
||||
game: str = "Kingdom Hearts 2"
|
||||
|
||||
|
||||
class LocationData(typing.NamedTuple):
|
||||
locid: int
|
||||
yml: str
|
||||
charName: str = "Sora"
|
||||
charNumber: int = 1
|
||||
|
||||
|
||||
class KH2Item(Item):
|
||||
game: str = "Kingdom Hearts 2"
|
||||
|
||||
|
||||
class ItemData(typing.NamedTuple):
|
||||
quantity: int = 0
|
||||
kh2id: int = 0
|
||||
# Save+ mem addr
|
||||
memaddr: int = 0
|
||||
# some items have bitmasks. if bitmask>0 bitor to give item else
|
||||
bitmask: int = 0
|
||||
# if ability then
|
||||
ability: bool = False
|
||||
@@ -12,7 +12,6 @@ from .OpenKH import patch_kh2
|
||||
from .Options import KingdomHearts2Options
|
||||
from .Regions import create_regions, connect_regions
|
||||
from .Rules import *
|
||||
from .Subclasses import KH2Item
|
||||
|
||||
|
||||
def launch_client():
|
||||
@@ -50,9 +49,7 @@ class KH2World(World):
|
||||
for item_id, item in enumerate(item_dictionary_table.keys(), 0x130000)}
|
||||
location_name_to_id = {item: location
|
||||
for location, item in enumerate(all_locations.keys(), 0x130000)}
|
||||
|
||||
item_name_groups = item_groups
|
||||
location_name_groups = location_groups
|
||||
|
||||
visitlocking_dict: Dict[str, int]
|
||||
plando_locations: Dict[str, str]
|
||||
@@ -256,8 +253,11 @@ class KH2World(World):
|
||||
self.goofy_gen_early()
|
||||
self.keyblade_gen_early()
|
||||
|
||||
# final xemnas isn't a location anymore
|
||||
# self.total_locations -= 1
|
||||
if self.multiworld.FinalXemnas[self.player]:
|
||||
self.plando_locations[LocationName.FinalXemnas] = ItemName.Victory
|
||||
else:
|
||||
self.plando_locations[LocationName.FinalXemnas] = self.create_filler().name
|
||||
self.total_locations -= 1
|
||||
|
||||
if self.options.WeaponSlotStartHint:
|
||||
for location in all_weapon_slot:
|
||||
|
||||
@@ -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,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?
|
||||
|
||||
@@ -399,26 +399,6 @@ class Palette(Choice):
|
||||
option_pink = 4
|
||||
option_inverted = 5
|
||||
|
||||
class Music(Choice, LADXROption):
|
||||
"""
|
||||
[Vanilla] Regular Music
|
||||
[Shuffled] Shuffled Music
|
||||
[Off] No music
|
||||
"""
|
||||
ladxr_name = "music"
|
||||
option_vanilla = 0
|
||||
option_shuffled = 1
|
||||
option_off = 2
|
||||
|
||||
|
||||
def to_ladxr_option(self, all_options):
|
||||
s = ""
|
||||
if self.value == self.option_shuffled:
|
||||
s = "random"
|
||||
elif self.value == self.option_off:
|
||||
s = "off"
|
||||
return self.ladxr_name, s
|
||||
|
||||
class WarpImprovements(DefaultOffToggle):
|
||||
"""
|
||||
[On] Adds remake style warp screen to the game. Choose your warp destination on the map after jumping in a portal and press B to select.
|
||||
@@ -464,7 +444,6 @@ links_awakening_options: typing.Dict[str, typing.Type[Option]] = {
|
||||
'shuffle_maps': ShuffleMaps,
|
||||
'shuffle_compasses': ShuffleCompasses,
|
||||
'shuffle_stone_beaks': ShuffleStoneBeaks,
|
||||
'music': Music,
|
||||
'music_change_condition': MusicChangeCondition,
|
||||
'nag_messages': NagMessages,
|
||||
'ap_title_screen': APTitleScreen,
|
||||
|
||||
@@ -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...**
|
||||
|
||||
@@ -30,8 +30,8 @@ guide: [Basic Multiworld Setup Guide](/tutorial/Archipelago/setup/en)
|
||||
|
||||
### Where do I get a config file?
|
||||
|
||||
The [Player Settings Page](/games/Landstalker%20-%20The%20Treasures%20of%20King%20Nole/player-settings) on the website allows
|
||||
you to easily configure your personal settings
|
||||
The [Player Settings Page](../player-settings) on the website allows you to easily configure your personal settings
|
||||
and export a config file from them.
|
||||
|
||||
## How-to-play
|
||||
|
||||
|
||||
@@ -1118,13 +1118,7 @@
|
||||
id: Cross Room/Panel_north_missing
|
||||
colors: green
|
||||
tag: forbid
|
||||
required_panel:
|
||||
- room: Outside The Bold
|
||||
panel: MOUTH
|
||||
- room: Outside The Bold
|
||||
panel: YEAST
|
||||
- room: Outside The Bold
|
||||
panel: WET
|
||||
required_room: Outside The Bold
|
||||
DIAMONDS:
|
||||
id: Cross Room/Panel_diamonds_missing
|
||||
colors: green
|
||||
@@ -2641,6 +2635,12 @@
|
||||
panels:
|
||||
- OBSTACLE
|
||||
The Colorful:
|
||||
# The set of required_doors in the achievement panel should prevent
|
||||
# generation from asking you to solve The Colorful before opening all of the
|
||||
# doors. Access from the roof is included so that the painting here could be
|
||||
# an entrance. The client will have to be hardcoded to not open the door to
|
||||
# the achievement until all of the doors are open, whether by solving the
|
||||
# panels or through receiving items.
|
||||
entrances:
|
||||
The Colorful (Gray):
|
||||
room: The Colorful (Gray)
|
||||
@@ -2651,53 +2651,31 @@
|
||||
id: Countdown Panels/Panel_colorful_colorful
|
||||
check: True
|
||||
tag: forbid
|
||||
required_panel:
|
||||
required_door:
|
||||
- room: The Colorful (White)
|
||||
panel: BEGIN
|
||||
door: Progress Door
|
||||
- room: The Colorful (Black)
|
||||
panel: FOUND
|
||||
door: Progress Door
|
||||
- room: The Colorful (Red)
|
||||
panel: LOAF
|
||||
door: Progress Door
|
||||
- room: The Colorful (Yellow)
|
||||
panel: CREAM
|
||||
door: Progress Door
|
||||
- room: The Colorful (Blue)
|
||||
panel: SUN
|
||||
door: Progress Door
|
||||
- room: The Colorful (Purple)
|
||||
panel: SPOON
|
||||
door: Progress Door
|
||||
- room: The Colorful (Orange)
|
||||
panel: LETTERS
|
||||
door: Progress Door
|
||||
- room: The Colorful (Green)
|
||||
panel: WALLS
|
||||
door: Progress Door
|
||||
- room: The Colorful (Brown)
|
||||
panel: IRON
|
||||
door: Progress Door
|
||||
- room: The Colorful (Gray)
|
||||
panel: OBSTACLE
|
||||
door: Progress Door
|
||||
achievement: The Colorful
|
||||
paintings:
|
||||
- id: arrows_painting_12
|
||||
orientation: north
|
||||
progression:
|
||||
Progressive Colorful:
|
||||
- room: The Colorful (White)
|
||||
door: Progress Door
|
||||
- room: The Colorful (Black)
|
||||
door: Progress Door
|
||||
- room: The Colorful (Red)
|
||||
door: Progress Door
|
||||
- room: The Colorful (Yellow)
|
||||
door: Progress Door
|
||||
- room: The Colorful (Blue)
|
||||
door: Progress Door
|
||||
- room: The Colorful (Purple)
|
||||
door: Progress Door
|
||||
- room: The Colorful (Orange)
|
||||
door: Progress Door
|
||||
- room: The Colorful (Green)
|
||||
door: Progress Door
|
||||
- room: The Colorful (Brown)
|
||||
door: Progress Door
|
||||
- room: The Colorful (Gray)
|
||||
door: Progress Door
|
||||
Welcome Back Area:
|
||||
entrances:
|
||||
Starting Room:
|
||||
@@ -4224,6 +4202,9 @@
|
||||
SIX:
|
||||
id: Backside Room/Panel_six_six_5
|
||||
tag: midwhite
|
||||
colors:
|
||||
- red
|
||||
- yellow
|
||||
hunt: True
|
||||
required_door:
|
||||
room: Number Hunt
|
||||
@@ -4299,6 +4280,9 @@
|
||||
SIX:
|
||||
id: Backside Room/Panel_six_six_6
|
||||
tag: midwhite
|
||||
colors:
|
||||
- red
|
||||
- yellow
|
||||
hunt: True
|
||||
required_door:
|
||||
room: Number Hunt
|
||||
@@ -4420,14 +4404,9 @@
|
||||
colors: blue
|
||||
tag: forbid
|
||||
required_panel:
|
||||
- room: The Bearer (West)
|
||||
panel: SMILE
|
||||
- room: Outside The Bold
|
||||
panel: MOUTH
|
||||
- room: Outside The Bold
|
||||
panel: YEAST
|
||||
- room: Outside The Bold
|
||||
panel: WET
|
||||
room: The Bearer (West)
|
||||
panel: SMILE
|
||||
required_room: Outside The Bold
|
||||
Cross Tower (South):
|
||||
entrances: # No roof access
|
||||
The Bearer (North):
|
||||
|
||||
@@ -1452,4 +1452,3 @@ progression:
|
||||
Progressive Fearless: 444470
|
||||
Progressive Orange Tower: 444482
|
||||
Progressive Art Gallery: 444563
|
||||
Progressive Colorful: 444580
|
||||
|
||||
@@ -28,10 +28,6 @@ class ItemData(NamedTuple):
|
||||
# door shuffle is on and tower isn't progressive
|
||||
return world.options.shuffle_doors != ShuffleDoors.option_none \
|
||||
and not world.options.progressive_orange_tower
|
||||
elif self.mode == "the colorful":
|
||||
# complex door shuffle is on and colorful isn't progressive
|
||||
return world.options.shuffle_doors == ShuffleDoors.option_complex \
|
||||
and not world.options.progressive_colorful
|
||||
elif self.mode == "complex door":
|
||||
return world.options.shuffle_doors == ShuffleDoors.option_complex
|
||||
elif self.mode == "door group":
|
||||
@@ -74,8 +70,6 @@ def load_item_data():
|
||||
if room_name in PROGRESSION_BY_ROOM and door_name in PROGRESSION_BY_ROOM[room_name]:
|
||||
if room_name == "Orange Tower":
|
||||
door_mode = "orange tower"
|
||||
elif room_name == "The Colorful":
|
||||
door_mode = "the colorful"
|
||||
else:
|
||||
door_mode = "special"
|
||||
|
||||
|
||||
@@ -21,13 +21,6 @@ class ProgressiveOrangeTower(DefaultOnToggle):
|
||||
display_name = "Progressive Orange Tower"
|
||||
|
||||
|
||||
class ProgressiveColorful(DefaultOnToggle):
|
||||
"""When "Shuffle Doors" is on "complex", this setting governs the manner in which The Colorful opens up.
|
||||
If off, there is an item for each room of The Colorful, meaning that random rooms in the middle of the sequence can open up without giving you access to them.
|
||||
If on, there are ten progressive items, which open up the sequence from White forward."""
|
||||
display_name = "Progressive Colorful"
|
||||
|
||||
|
||||
class LocationChecks(Choice):
|
||||
"""On "normal", there will be a location check for each panel set that would ordinarily open a door, as well as for
|
||||
achievement panels and a small handful of other panels.
|
||||
@@ -124,7 +117,6 @@ class DeathLink(Toggle):
|
||||
class LingoOptions(PerGameCommonOptions):
|
||||
shuffle_doors: ShuffleDoors
|
||||
progressive_orange_tower: ProgressiveOrangeTower
|
||||
progressive_colorful: ProgressiveColorful
|
||||
location_checks: LocationChecks
|
||||
shuffle_colors: ShuffleColors
|
||||
shuffle_panels: ShufflePanels
|
||||
|
||||
@@ -83,8 +83,7 @@ class LingoPlayerLogic:
|
||||
|
||||
def handle_non_grouped_door(self, room_name: str, door_data: Door, world: "LingoWorld"):
|
||||
if room_name in PROGRESSION_BY_ROOM and door_data.name in PROGRESSION_BY_ROOM[room_name]:
|
||||
if (room_name == "Orange Tower" and not world.options.progressive_orange_tower)\
|
||||
or (room_name == "The Colorful" and not world.options.progressive_colorful):
|
||||
if room_name == "Orange Tower" and not world.options.progressive_orange_tower:
|
||||
self.set_door_item(room_name, door_data.name, door_data.item_name)
|
||||
else:
|
||||
progressive_item_name = PROGRESSION_BY_ROOM[room_name][door_data.name].item_name
|
||||
@@ -224,7 +223,7 @@ class LingoPlayerLogic:
|
||||
"kind of logic error.")
|
||||
|
||||
if door_shuffle != ShuffleDoors.option_none and location_classification != LocationClassification.insanity \
|
||||
and not early_color_hallways:
|
||||
and not early_color_hallways 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
|
||||
|
||||
@@ -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...**
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -34,7 +34,6 @@ class MuseDashCollections:
|
||||
"Rush-Hour",
|
||||
"Find this Month's Featured Playlist",
|
||||
"PeroPero in the Universe",
|
||||
"umpopoff"
|
||||
]
|
||||
|
||||
album_items: Dict[str, AlbumData] = {}
|
||||
@@ -82,22 +81,11 @@ class MuseDashCollections:
|
||||
steamer_mode = sections[3] == "True"
|
||||
|
||||
if song_name in self.DIFF_OVERRIDES:
|
||||
# These songs use non-standard difficulty values. Which are being overriden with standard values.
|
||||
# But also avoid filling any missing difficulties (i.e. 0s) with a difficulty value.
|
||||
if sections[4] != '0':
|
||||
diff_of_easy = 4
|
||||
else:
|
||||
diff_of_easy = None
|
||||
|
||||
if sections[5] != '0':
|
||||
diff_of_hard = 7
|
||||
else:
|
||||
diff_of_hard = None
|
||||
|
||||
if sections[6] != '0':
|
||||
diff_of_master = 10
|
||||
else:
|
||||
diff_of_master = None
|
||||
# Note: These difficulties may not actually be representative of these songs.
|
||||
# The game does not provide these difficulties so they have to be filled in.
|
||||
diff_of_easy = 4
|
||||
diff_of_hard = 7
|
||||
diff_of_master = 10
|
||||
else:
|
||||
diff_of_easy = self.parse_song_difficulty(sections[4])
|
||||
diff_of_hard = self.parse_song_difficulty(sections[5])
|
||||
|
||||
@@ -119,7 +119,7 @@ Prestige and Vestige|56-4|Give Up TREATMENT Vol.11|True|6|8|11|
|
||||
Tiny Fate|56-5|Give Up TREATMENT Vol.11|False|7|9|11|
|
||||
Tsuki ni Murakumo Hana ni Kaze|55-0|Touhou Mugakudan -2-|False|3|5|7|
|
||||
Patchouli's - Best Hit GSK|55-1|Touhou Mugakudan -2-|False|3|5|8|
|
||||
Monosugoi Space Shuttle de Koishi ga Monosugoi uta|55-2|Touhou Mugakudan -2-|False|3|5|7|11
|
||||
Monosugoi Space Shuttle de Koishi ga Monosugoi uta|55-2|Touhou Mugakudan -2-|False|3|5|7|
|
||||
Kakoinaki Yo wa Ichigo no Tsukikage|55-3|Touhou Mugakudan -2-|False|3|6|8|
|
||||
Psychedelic Kizakura Doumei|55-4|Touhou Mugakudan -2-|False|4|7|10|
|
||||
Mischievous Sensation|55-5|Touhou Mugakudan -2-|False|5|7|9|
|
||||
@@ -501,12 +501,4 @@ slic.hertz|68-1|Gambler's Tricks|True|5|7|9|
|
||||
Fuzzy-Navel|68-2|Gambler's Tricks|True|6|8|10|11
|
||||
Swing Edge|68-3|Gambler's Tricks|True|4|8|10|
|
||||
Twisted Escape|68-4|Gambler's Tricks|True|5|8|10|11
|
||||
Swing Sweet Twee Dance|68-5|Gambler's Tricks|False|4|7|10|
|
||||
Sanyousei SAY YA!!!|43-42|MD Plus Project|False|4|6|8|
|
||||
YUKEMURI TAMAONSEN II|43-43|MD Plus Project|False|3|6|9|
|
||||
Samayoi no mei Amatsu|69-0|Touhou Mugakudan -3-|False|4|6|9|
|
||||
INTERNET SURVIVOR|69-1|Touhou Mugakudan -3-|False|5|8|10|
|
||||
Shuki*RaiRai|69-2|Touhou Mugakudan -3-|False|5|7|9|
|
||||
HELLOHELL|69-3|Touhou Mugakudan -3-|False|4|7|10|
|
||||
Calamity Fortune|69-4|Touhou Mugakudan -3-|True|6|8|10|11
|
||||
Tsurupettan|69-5|Touhou Mugakudan -3-|True|2|5|8|
|
||||
Swing Sweet Twee Dance|68-5|Gambler's Tricks|False|4|7|10|
|
||||
@@ -36,7 +36,7 @@ class AdditionalSongs(Range):
|
||||
- The final song count may be lower due to other settings.
|
||||
"""
|
||||
range_start = 15
|
||||
range_end = 508 # Note will probably not reach this high if any other settings are done.
|
||||
range_end = 500 # Note will probably not reach this high if any other settings are done.
|
||||
default = 40
|
||||
display_name = "Additional Song Count"
|
||||
|
||||
|
||||
@@ -328,6 +328,5 @@ class MuseDashWorld(World):
|
||||
"victoryLocation": self.victory_song_name,
|
||||
"deathLink": self.options.death_link.value,
|
||||
"musicSheetWinCount": self.get_music_sheet_win_count(),
|
||||
"gradeNeeded": self.options.grade_needed.value,
|
||||
"hasFiller": True,
|
||||
"gradeNeeded": self.options.grade_needed.value
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user