mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-03-10 17:43:53 -07:00
Compare commits
93 Commits
use_self.o
...
commonclie
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8fdad3cbba | ||
|
|
aa72f671bc | ||
|
|
5f9ce2b7b6 | ||
|
|
1307754f02 | ||
|
|
ac7b707e3e | ||
|
|
ec440b7785 | ||
|
|
4c901dcfc0 | ||
|
|
834b6e35b4 | ||
|
|
602c2966fc | ||
|
|
49ecd4b9c1 | ||
|
|
de8fe21d4a | ||
|
|
4fdeec4f70 | ||
|
|
71a3e2230d | ||
|
|
325a510ba7 | ||
|
|
5dcaa6ca20 | ||
|
|
e15873e861 | ||
|
|
5c7bae7940 | ||
|
|
e6f7ed5060 | ||
|
|
1c2dcb7b01 | ||
|
|
5df7a8f686 | ||
|
|
3a588099bd | ||
|
|
d000b52ae0 | ||
|
|
fe3bc8d6be | ||
|
|
7affb885ba | ||
|
|
d390d2eff8 | ||
|
|
0efc13fc8a | ||
|
|
c6896c6af9 | ||
|
|
adad7b532d | ||
|
|
d756960a0b | ||
|
|
30ec080449 | ||
|
|
79e2f7e357 | ||
|
|
b4077a0717 | ||
|
|
518b04c08e | ||
|
|
d10f8f66c7 | ||
|
|
6d393fe42b | ||
|
|
5b93db121f | ||
|
|
ad074490bc | ||
|
|
6ac3d5c651 | ||
|
|
ed6b7b2670 | ||
|
|
6904bd5885 | ||
|
|
962b9b28f0 | ||
|
|
37b03807fd | ||
|
|
73e41cb701 | ||
|
|
cfd758168c | ||
|
|
01fb44c186 | ||
|
|
2725c0258f | ||
|
|
0c0adb0745 | ||
|
|
4a85f21c25 | ||
|
|
3933fd3929 | ||
|
|
b241644e54 | ||
|
|
e00b5a7d17 | ||
|
|
47dd36456e | ||
|
|
4ce8a7ec4d | ||
|
|
a99c1e15ad | ||
|
|
44de140add | ||
|
|
ac2387e17c | ||
|
|
2760deb5b6 | ||
|
|
f530895c33 | ||
|
|
b6f3ccb8c5 | ||
|
|
388413fcdd | ||
|
|
4045c6a9cf | ||
|
|
e082c83dc7 | ||
|
|
82410fd554 | ||
|
|
570ba28bee | ||
|
|
b0638b993d | ||
|
|
89f211f31e | ||
|
|
70fdd6b90d | ||
|
|
f22daca74e | ||
|
|
064a7bf01b | ||
|
|
02a9430ad5 | ||
|
|
c19afa4f4e | ||
|
|
c593a960f6 | ||
|
|
7406a1e512 | ||
|
|
0df0955415 | ||
|
|
bf17582c55 | ||
|
|
e5c739ee31 | ||
|
|
88c7484b3a | ||
|
|
c104e81145 | ||
|
|
3d1be0c468 | ||
|
|
e674e37e08 | ||
|
|
d1a17a350d | ||
|
|
24ac3de125 | ||
|
|
901201f675 | ||
|
|
c7617f92dd | ||
|
|
8e708f829d | ||
|
|
7af654e619 | ||
|
|
af1f6e9113 | ||
|
|
04d194db74 | ||
|
|
70eb2b58f5 | ||
|
|
576c705106 | ||
|
|
b99c734954 | ||
|
|
7c70b87f29 | ||
|
|
2512eb7501 |
5
.coveragerc
Normal file
5
.coveragerc
Normal file
@@ -0,0 +1,5 @@
|
||||
[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=10 --max-doc-length=120 --max-line-length=120 --statistics ${{ env.diff }}
|
||||
flake8 --count --max-complexity=14 --max-doc-length=120 --max-line-length=120 --statistics ${{ env.diff }}
|
||||
|
||||
- name: "mypy: Type check modified files"
|
||||
continue-on-error: true
|
||||
|
||||
18
.run/Archipelago Unittests.run.xml
Normal file
18
.run/Archipelago Unittests.run.xml
Normal file
@@ -0,0 +1,18 @@
|
||||
<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,9 +1056,6 @@ 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": list(needed_updates)}])
|
||||
await self.send_msgs([{"cmd": "GetDataPackage", "games": [game_name]} for game_name in needed_updates])
|
||||
|
||||
def update_game(self, game_package: dict):
|
||||
for item_name, item_id in game_package["item_name_to_id"].items():
|
||||
@@ -477,6 +477,7 @@ 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)
|
||||
|
||||
@@ -611,6 +612,10 @@ async def server_loop(ctx: CommonContext, address: typing.Optional[str] = None)
|
||||
|
||||
address = f"ws://{address}" if "://" not in address \
|
||||
else address.replace("archipelago://", "ws://")
|
||||
uri = urllib.parse.urlparse(address)
|
||||
if uri.username and uri.password is None:
|
||||
# Fix for Firefox stripping empty password https://bugzilla.mozilla.org/show_bug.cgi?id=1876952
|
||||
address = address.replace("@", ":@")
|
||||
|
||||
server_url = urllib.parse.urlparse(address)
|
||||
if server_url.username:
|
||||
@@ -727,7 +732,6 @@ 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':
|
||||
|
||||
12
Main.py
12
Main.py
@@ -114,7 +114,9 @@ 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 world.start_inventory_from_pool.setdefault(player, StartInventoryPool({})).value.items():
|
||||
for item_name, count in getattr(world.worlds[player].options,
|
||||
"start_inventory_from_pool",
|
||||
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.
|
||||
@@ -167,10 +169,14 @@ 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(world.start_inventory_from_pool[player].value for player in world.player_ids):
|
||||
if any(getattr(world.worlds[player].options, "start_inventory_from_pool", None) for player in world.player_ids):
|
||||
new_items: List[Item] = []
|
||||
depletion_pool: Dict[int, Dict[str, int]] = {
|
||||
player: world.start_inventory_from_pool[player].value.copy() for player in world.player_ids}
|
||||
player: getattr(world.worlds[player].options,
|
||||
"start_inventory_from_pool",
|
||||
StartInventoryPool({})).value.copy()
|
||||
for player in world.player_ids
|
||||
}
|
||||
for player, items in depletion_pool.items():
|
||||
player_world: AutoWorld.World = world.worlds[player]
|
||||
for count in items.values():
|
||||
|
||||
@@ -4,14 +4,29 @@ 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)
|
||||
update_ran = getattr(sys, "frozen", False) or multiprocessing.parent_process()
|
||||
_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'),))
|
||||
|
||||
if not update_ran:
|
||||
for entry in os.scandir(os.path.join(local_dir, "worlds")):
|
||||
|
||||
@@ -2210,25 +2210,24 @@ 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():
|
||||
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.")
|
||||
inactivity_shutdown()
|
||||
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:
|
||||
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.")
|
||||
inactivity_shutdown()
|
||||
else:
|
||||
await asyncio.sleep(seconds)
|
||||
|
||||
|
||||
@@ -58,6 +58,7 @@ 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:
|
||||
file = request.files['file']
|
||||
options = get_yaml_data(file)
|
||||
files = request.files.getlist('file')
|
||||
options = get_yaml_data(files)
|
||||
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="goal">Allow !collect after goal completion</option>
|
||||
<option value="auto">Automatic on goal completion</option>
|
||||
<option value="goal">Allow !collect after 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 2023 Archipelago</div>
|
||||
<div id="copyright-notice">Copyright 2024 Archipelago</div>
|
||||
<div id="links">
|
||||
<a href="/sitemap">Site Map</a>
|
||||
-
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import datetime
|
||||
import collections
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Callable, Dict, List, Optional, Set, Tuple
|
||||
from uuid import UUID
|
||||
@@ -8,7 +9,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
|
||||
from Utils import restricted_loads, KeyedDefaultDict
|
||||
from . import app, cache
|
||||
from .models import GameDataPackage, Room
|
||||
|
||||
@@ -62,12 +63,18 @@ 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]] = {}
|
||||
self.location_id_to_name: Dict[str, Dict[int, str]] = {}
|
||||
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})")
|
||||
})
|
||||
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] = {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()}
|
||||
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()})
|
||||
|
||||
# Normal lookup tables as well.
|
||||
self.item_name_to_id[game] = game_package["item_name_to_id"]
|
||||
@@ -115,10 +122,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) -> Dict[int, int]:
|
||||
def get_player_inventory_counts(self, team: int, player: int) -> collections.Counter:
|
||||
"""Retrieves a dictionary of all items received by their id and their received count."""
|
||||
items = self.get_player_received_items(team, player)
|
||||
inventory = {item: 0 for item in self.item_id_to_name[self.get_player_game(team, player)]}
|
||||
inventory = collections.Counter()
|
||||
for item in items:
|
||||
inventory[item.item] += 1
|
||||
|
||||
@@ -149,16 +156,15 @@ 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 if self.get_slot_info(team, player).type == SlotType.player
|
||||
) for team, players in self.get_team_players().items()
|
||||
self.get_player_client_status(team, player) == ClientStatus.CLIENT_GOAL for player in players
|
||||
) for team, players in self.get_all_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_team_players().items():
|
||||
for team, players in self.get_all_slots().items():
|
||||
hints[team] = set()
|
||||
for player in players:
|
||||
hints[team] |= self.get_player_hints(team, player)
|
||||
@@ -170,7 +176,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_team_players().items()
|
||||
for team, players in self.get_all_players().items()
|
||||
}
|
||||
|
||||
@_cache_results
|
||||
@@ -178,16 +184,30 @@ 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_team_players().items()
|
||||
for team, players in self.get_all_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_team_players(self) -> Dict[int, List[int]]:
|
||||
def get_all_slots(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()]
|
||||
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
|
||||
]
|
||||
}
|
||||
|
||||
@_cache_results
|
||||
@@ -203,7 +223,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_team_players().items() for player in players
|
||||
for team, players in self.get_all_players().items() for player in players
|
||||
}
|
||||
|
||||
@_cache_results
|
||||
@@ -211,7 +231,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_team_players().items() for player in players
|
||||
for team, players in self.get_all_slots().items() for player in players
|
||||
}
|
||||
|
||||
@_cache_results
|
||||
@@ -219,7 +239,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_team_players().items() for player in players
|
||||
for team, players in self.get_all_players().items() for player in players
|
||||
}
|
||||
|
||||
@_cache_results
|
||||
@@ -227,14 +247,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_team_players().items() for player in players
|
||||
for team, players in self.get_all_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_team_players().items():
|
||||
for team, players in self.get_all_slots().items():
|
||||
for player in players:
|
||||
alias = self.get_player_alias(team, player)
|
||||
if alias:
|
||||
@@ -370,7 +390,8 @@ def render_generic_multiworld_tracker(tracker_data: TrackerData, enabled_tracker
|
||||
enabled_trackers=enabled_trackers,
|
||||
current_tracker="Generic",
|
||||
room=tracker_data.room,
|
||||
room_players=tracker_data.get_team_players(),
|
||||
all_slots=tracker_data.get_all_slots(),
|
||||
room_players=tracker_data.get_all_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(),
|
||||
@@ -389,7 +410,6 @@ 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
|
||||
|
||||
@@ -400,7 +420,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_team_players().items() for player in players
|
||||
} for team, players in tracker_data.get_all_slots().items() for player in players
|
||||
if tracker_data.get_player_game(team, player) == "Factorio"
|
||||
}
|
||||
|
||||
@@ -409,7 +429,8 @@ if "Factorio" in network_data_package["games"]:
|
||||
enabled_trackers=enabled_trackers,
|
||||
current_tracker="Factorio",
|
||||
room=tracker_data.room,
|
||||
room_players=tracker_data.get_team_players(),
|
||||
all_slots=tracker_data.get_all_slots(),
|
||||
room_players=tracker_data.get_all_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(),
|
||||
@@ -547,7 +568,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_team_players().items()
|
||||
for team, players in tracker_data.get_all_slots().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"
|
||||
@@ -585,7 +606,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_team_players().items()
|
||||
for team, players in tracker_data.get_all_slots().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"
|
||||
@@ -593,15 +614,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_team_players().items()
|
||||
for team, players in tracker_data.get_all_slots().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_team_players()[0]}
|
||||
player_small_key_locations = {player: set() for player in tracker_data.get_team_players()[0]}
|
||||
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]}
|
||||
group_big_key_locations = set()
|
||||
group_key_locations = set()
|
||||
|
||||
@@ -639,7 +660,8 @@ 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,
|
||||
room_players=tracker_data.get_team_players(),
|
||||
all_slots=tracker_data.get_all_slots(),
|
||||
room_players=tracker_data.get_all_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,11 +11,14 @@ 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
|
||||
|
||||
@@ -23,6 +26,15 @@ 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)
|
||||
@@ -37,6 +49,8 @@ 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()
|
||||
@@ -45,11 +59,19 @@ 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
|
||||
@@ -64,14 +86,15 @@ 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,505 +1,10 @@
|
||||
import asyncio
|
||||
import base64
|
||||
import platform
|
||||
from typing import Any, ClassVar, Coroutine, Dict, List, Optional, Protocol, Tuple, cast
|
||||
import ModuleUpdate
|
||||
ModuleUpdate.update()
|
||||
|
||||
# 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()
|
||||
import Utils # noqa: E402
|
||||
|
||||
from worlds.zillion.client import launch # noqa: E402
|
||||
|
||||
if __name__ == "__main__":
|
||||
Utils.init_logging("ZillionClient", exception_logger="Client")
|
||||
|
||||
colorama.init()
|
||||
asyncio.run(main())
|
||||
colorama.deinit()
|
||||
launch()
|
||||
|
||||
@@ -456,6 +456,7 @@ 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,6 +164,9 @@
|
||||
# The Legend of Zelda (1)
|
||||
/worlds/tloz/ @Rosalie-A @t3hf1gm3nt
|
||||
|
||||
# TUNIC
|
||||
/worlds/tunic/ @silent-destroyer
|
||||
|
||||
# Undertale
|
||||
/worlds/undertale/ @jonloveslegos
|
||||
|
||||
|
||||
@@ -380,12 +380,13 @@ 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. |
|
||||
| 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. |
|
||||
| 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. |
|
||||
|
||||
### 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.
|
||||
@@ -674,8 +675,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, 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. 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. |
|
||||
|
||||
@@ -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.32.31332') < 0);
|
||||
Result := (CompareStr(strVersion, 'v14.38.33130') < 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.2.1
|
||||
kivy>=2.3.0
|
||||
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("goal")
|
||||
collect_mode: CollectMode = CollectMode("goal")
|
||||
release_mode: ReleaseMode = ReleaseMode("auto")
|
||||
collect_mode: CollectMode = CollectMode("auto")
|
||||
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")
|
||||
plando_options: PlandoOptions = PlandoOptions("bosses, connections, texts")
|
||||
|
||||
|
||||
class SNIOptions(Group):
|
||||
|
||||
15
setup.py
15
setup.py
@@ -54,7 +54,6 @@ 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
|
||||
@@ -76,7 +75,6 @@ non_apworlds: set = {
|
||||
"Ocarina of Time",
|
||||
"Overcooked! 2",
|
||||
"Raft",
|
||||
"Secret of Evermore",
|
||||
"Slay the Spire",
|
||||
"Sudoku",
|
||||
"Super Mario 64",
|
||||
@@ -305,7 +303,6 @@ 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
|
||||
@@ -352,6 +349,18 @@ 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.exclude_locations[1].value
|
||||
excluded = self.multiworld.worlds[1].options.exclude_locations.value
|
||||
state = self.multiworld.get_all_state(False)
|
||||
for location in self.multiworld.get_locations():
|
||||
if location.name not in excluded:
|
||||
|
||||
127
test/benchmark/__init__.py
Normal file
127
test/benchmark/__init__.py
Normal file
@@ -0,0 +1,127 @@
|
||||
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,5 +1,6 @@
|
||||
import unittest
|
||||
from worlds.AutoWorld import AutoWorldRegister
|
||||
|
||||
from worlds.AutoWorld import AutoWorldRegister, call_all
|
||||
from . import setup_solo_multiworld
|
||||
|
||||
|
||||
@@ -53,7 +54,7 @@ class TestBase(unittest.TestCase):
|
||||
f"{game_name} Item count MUST meet or exceed the number of locations",
|
||||
)
|
||||
|
||||
def testItemsInDatapackage(self):
|
||||
def test_items_in_datapackage(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):
|
||||
@@ -69,3 +70,20 @@ 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,3 +10,10 @@ 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.exclude_locations[1].value
|
||||
excluded = world.worlds[1].options.exclude_locations.value
|
||||
state = world.get_all_state(False)
|
||||
for location in world.get_locations():
|
||||
if location.name not in excluded:
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import io
|
||||
import unittest
|
||||
import json
|
||||
import yaml
|
||||
|
||||
|
||||
class TestDocs(unittest.TestCase):
|
||||
@@ -23,7 +25,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(self):
|
||||
def test_generation_queued_weights(self):
|
||||
options = {
|
||||
"Tester1":
|
||||
{
|
||||
@@ -40,3 +42,19 @@ 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,24 +1,12 @@
|
||||
""" 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
|
||||
from .texture import FillType_Drawable, FillType_Vec, Texture
|
||||
|
||||
|
||||
class FillType_Shape(FillType_Drawable):
|
||||
texture: FillType_Texture
|
||||
texture: Texture
|
||||
|
||||
def __init__(self,
|
||||
*,
|
||||
texture: FillType_Texture = ...,
|
||||
texture: Texture = ...,
|
||||
pos: FillType_Vec = ...,
|
||||
size: FillType_Vec = ...) -> None: ...
|
||||
|
||||
@@ -35,6 +23,6 @@ class Rectangle(FillType_Shape):
|
||||
def __init__(self,
|
||||
*,
|
||||
source: str = ...,
|
||||
texture: FillType_Texture = ...,
|
||||
texture: Texture = ...,
|
||||
pos: FillType_Vec = ...,
|
||||
size: FillType_Vec = ...) -> None: ...
|
||||
13
typings/kivy/graphics/texture.pyi
Normal file
13
typings/kivy/graphics/texture.pyi
Normal file
@@ -0,0 +1,13 @@
|
||||
""" 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
|
||||
9
typings/kivy/uix/image.pyi
Normal file
9
typings/kivy/uix/image.pyi
Normal file
@@ -0,0 +1,9 @@
|
||||
import io
|
||||
|
||||
from kivy.graphics.texture import Texture
|
||||
|
||||
|
||||
class CoreImage:
|
||||
texture: Texture
|
||||
|
||||
def __init__(self, data: io.BytesIO, ext: str) -> None: ...
|
||||
@@ -79,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__:
|
||||
from warnings import warn
|
||||
warn("Assigning options through option_definitions is now deprecated. Use options_dataclass instead.")
|
||||
logging.warning(f"{name} Assigned options through option_definitions which is now deprecated. "
|
||||
"Please use options_dataclass instead.")
|
||||
dct["options_dataclass"] = make_dataclass(f"{name}Options", dct["option_definitions"].items(),
|
||||
bases=(PerGameCommonOptions,))
|
||||
|
||||
@@ -328,7 +328,7 @@ class World(metaclass=AutoWorldRegister):
|
||||
|
||||
def create_items(self) -> None:
|
||||
"""
|
||||
Method for creating and submitting items to the itempool. Items and Regions should *not* be created and submitted
|
||||
Method for creating and submitting items to the itempool. Items and Regions must *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
|
||||
|
||||
@@ -10,8 +10,7 @@ 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)
|
||||
(select `Adventure Client` during installation).
|
||||
- The built-in Archipelago client, which can be installed [here](https://github.com/ArchipelagoMW/Archipelago/releases).
|
||||
- An Adventure NTSC ROM file. The Archipelago community cannot provide these.
|
||||
|
||||
## Configuring BizHawk
|
||||
|
||||
@@ -26,6 +26,13 @@ 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,8 +2,7 @@
|
||||
|
||||
## Required Software
|
||||
|
||||
- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases). Make sure to check the box for
|
||||
`SNI Client - A Link to the Past Patch Setup`
|
||||
- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases).
|
||||
- [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
|
||||
@@ -18,11 +17,12 @@ but it is not supported.**
|
||||
|
||||
## Installation Procedures
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
2. If you are using an emulator, you should assign your Lua capable emulator as your default program for launching ROM
|
||||
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.
|
||||
2. Right-click on a ROM file and select **Open with...**
|
||||
|
||||
@@ -7,16 +7,25 @@ from ..AutoWorld import WebWorld, World
|
||||
class Bk_SudokuWebWorld(WebWorld):
|
||||
options_page = "games/Sudoku/info/en"
|
||||
theme = 'partyTime'
|
||||
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']
|
||||
)
|
||||
]
|
||||
|
||||
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]
|
||||
|
||||
|
||||
class Bk_SudokuWorld(World):
|
||||
|
||||
21
worlds/bk_sudoku/docs/de_Sudoku.md
Normal file
21
worlds/bk_sudoku/docs/de_Sudoku.md
Normal file
@@ -0,0 +1,21 @@
|
||||
# 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.
|
||||
27
worlds/bk_sudoku/docs/setup_de.md
Normal file
27
worlds/bk_sudoku/docs/setup_de.md
Normal file
@@ -0,0 +1,27 @@
|
||||
# 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
|
||||
@@ -5,7 +5,6 @@
|
||||
- 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
|
||||
|
||||
|
||||
@@ -11,16 +11,26 @@ from .Rules import get_button_rule
|
||||
|
||||
class CliqueWebWorld(WebWorld):
|
||||
theme = "partyTime"
|
||||
tutorials = [
|
||||
Tutorial(
|
||||
tutorial_name="Start Guide",
|
||||
description="A guide to playing Clique.",
|
||||
language="English",
|
||||
file_name="guide_en.md",
|
||||
link="guide/en",
|
||||
authors=["Phar"]
|
||||
)
|
||||
]
|
||||
|
||||
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]
|
||||
|
||||
|
||||
class CliqueWorld(World):
|
||||
|
||||
18
worlds/clique/docs/de_Clique.md
Normal file
18
worlds/clique/docs/de_Clique.md
Normal file
@@ -0,0 +1,18 @@
|
||||
# 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.
|
||||
25
worlds/clique/docs/guide_de.md
Normal file
25
worlds/clique/docs/guide_de.md
Normal file
@@ -0,0 +1,25 @@
|
||||
# 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*~~
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
## Required Software
|
||||
|
||||
- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases). Make sure to check the box for `SNI Client - Donkey Kong Country 3 Patch Setup`
|
||||
- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases).
|
||||
|
||||
|
||||
- Hardware or software capable of loading and playing SNES ROM files
|
||||
@@ -23,9 +23,10 @@
|
||||
|
||||
### Windows Setup
|
||||
|
||||
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.
|
||||
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.
|
||||
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,14 +13,23 @@ client_version = 0
|
||||
|
||||
|
||||
class DLCqwebworld(WebWorld):
|
||||
tutorials = [Tutorial(
|
||||
setup_en = 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):
|
||||
|
||||
49
worlds/dlcquest/docs/fr_DLCQuest.md
Normal file
49
worlds/dlcquest/docs/fr_DLCQuest.md
Normal file
@@ -0,0 +1,49 @@
|
||||
# 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.
|
||||
55
worlds/dlcquest/docs/setup_fr.md
Normal file
55
worlds/dlcquest/docs/setup_fr.md
Normal file
@@ -0,0 +1,55 @@
|
||||
# # 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.
|
||||
@@ -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": 19, "stone-furnace": 19}
|
||||
default = {"burner-mining-drill": 4, "stone-furnace": 4, "raw-fish": 50}
|
||||
|
||||
|
||||
class FactorioFreeSampleBlacklist(OptionSet):
|
||||
|
||||
@@ -74,6 +74,7 @@ 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 == b'\x00' or check_2 == b'\x00':
|
||||
if check_1 in (b'\x00', b'\x55') or check_2 in (b'\x00', b'\x55'):
|
||||
return
|
||||
|
||||
def get_range(data_range):
|
||||
|
||||
@@ -223,11 +223,6 @@ 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). Make sure to check the box for `SNI Client`
|
||||
- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases).
|
||||
|
||||
- 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. 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.
|
||||
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. 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,4 +1,5 @@
|
||||
import collections
|
||||
import logging
|
||||
import typing
|
||||
|
||||
from BaseClasses import LocationProgressType, MultiWorld, Location, Region, Entrance
|
||||
@@ -81,15 +82,18 @@ def locality_rules(world: MultiWorld):
|
||||
i.name not in sending_blockers[i.player] and old_rule(i)
|
||||
|
||||
|
||||
def exclusion_rules(world: MultiWorld, player: int, exclude_locations: typing.Set[str]) -> None:
|
||||
def exclusion_rules(multiworld: MultiWorld, player: int, exclude_locations: typing.Set[str]) -> None:
|
||||
for loc_name in exclude_locations:
|
||||
try:
|
||||
location = world.get_location(loc_name, player)
|
||||
location = multiworld.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 world.worlds[player].location_name_to_id:
|
||||
if loc_name not in multiworld.worlds[player].location_name_to_id:
|
||||
raise Exception(f"Unable to exclude location {loc_name} in player {player}'s world.") from e
|
||||
else:
|
||||
location.progress_type = LocationProgressType.EXCLUDED
|
||||
if not location.event:
|
||||
location.progress_type = LocationProgressType.EXCLUDED
|
||||
else:
|
||||
logging.warning(f"Unable to exclude location {loc_name} in player {player}'s world.")
|
||||
|
||||
|
||||
def set_rule(spot: typing.Union["BaseClasses.Location", "BaseClasses.Entrance"], rule: CollectionRule):
|
||||
|
||||
@@ -17,19 +17,22 @@ 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.
|
||||
|
||||
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.
|
||||
Archipelago installations are automatically bundled with some programs. These include a launcher, a generator, a
|
||||
server and some clients.
|
||||
|
||||
The server will allow you to host the multiworld on your machine. Hosting on your machine requires forwarding the port
|
||||
- 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
|
||||
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. 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.
|
||||
- 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.
|
||||
|
||||
## Generating a game
|
||||
|
||||
@@ -72,14 +75,18 @@ 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, 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
|
||||
To generate a game on your local machine, make sure to install the Archipelago software. 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`, 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.
|
||||
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.
|
||||
|
||||
### Generating a multiplayer game
|
||||
|
||||
@@ -97,12 +104,9 @@ player name.
|
||||
|
||||
#### On the website
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
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.
|
||||
@@ -114,8 +118,11 @@ 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, 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`).
|
||||
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.
|
||||
|
||||
##### Changing local host settings for generation
|
||||
|
||||
@@ -123,10 +130,12 @@ 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. 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. 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`.
|
||||
|
||||
|
||||
## Hosting an Archipelago Server
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -444,6 +444,8 @@ 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,11 +80,6 @@ class KH2Context(CommonContext):
|
||||
},
|
||||
},
|
||||
}
|
||||
self.front_of_inventory = {
|
||||
"Sora": 0x2546,
|
||||
"Donald": 0x2658,
|
||||
"Goofy": 0x276C,
|
||||
}
|
||||
self.kh2seedname = None
|
||||
self.kh2slotdata = None
|
||||
self.itemamount = {}
|
||||
@@ -169,6 +164,14 @@ 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)
|
||||
@@ -219,6 +222,12 @@ 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']
|
||||
@@ -476,7 +485,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]
|
||||
@@ -507,6 +516,8 @@ 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]:
|
||||
@@ -518,11 +529,14 @@ 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
|
||||
elif itemname in self.goofy_ability_set:
|
||||
else:
|
||||
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"]:
|
||||
@@ -615,7 +629,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
|
||||
@@ -673,10 +687,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 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)
|
||||
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)
|
||||
|
||||
# remove the dummy level 1 growths if they are in these invo slots.
|
||||
for inventorySlot in {0x25CE, 0x25D0, 0x25D2, 0x25D4, 0x25D6, 0x25D8}:
|
||||
@@ -740,15 +754,60 @@ 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)
|
||||
@@ -762,7 +821,8 @@ class KH2Context(CommonContext):
|
||||
|
||||
def finishedGame(ctx: KH2Context, message):
|
||||
if ctx.kh2slotdata['FinalXemnas'] == 1:
|
||||
if not ctx.final_xemnas and ctx.kh2_loc_name_to_id[LocationName.FinalXemnas] in ctx.locations_checked:
|
||||
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:
|
||||
ctx.final_xemnas = True
|
||||
# three proofs
|
||||
if ctx.kh2slotdata['Goal'] == 0:
|
||||
|
||||
@@ -2,22 +2,7 @@ import typing
|
||||
|
||||
from BaseClasses import Item
|
||||
from .Names import ItemName
|
||||
|
||||
|
||||
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
|
||||
|
||||
from .Subclasses import ItemData
|
||||
|
||||
# 0x130000
|
||||
Reports_Table = {
|
||||
@@ -209,7 +194,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),
|
||||
@@ -349,7 +334,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,19 +1,9 @@
|
||||
import typing
|
||||
|
||||
from BaseClasses import Location
|
||||
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
|
||||
|
||||
from .Names import LocationName, ItemName, RegionName
|
||||
from .Subclasses import LocationData
|
||||
from .Regions import KH2REGIONS
|
||||
|
||||
# data's addrcheck sys3 addr obtained roomid bit index is eventid
|
||||
LoD_Checks = {
|
||||
@@ -541,7 +531,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"),
|
||||
}
|
||||
|
||||
@@ -806,74 +796,75 @@ 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.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,
|
||||
}
|
||||
all_weapon_slot = {
|
||||
LocationName.FAKESlot,
|
||||
@@ -1361,3 +1352,9 @@ 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 worlds.kh2 import default_itempool_option
|
||||
from . import default_itempool_option
|
||||
|
||||
|
||||
class SoraEXP(Range):
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import typing
|
||||
|
||||
from BaseClasses import MultiWorld, Region
|
||||
from . import Locations
|
||||
|
||||
from .Locations import KH2Location, event_location_to_item
|
||||
from . import LocationName, RegionName, Events_Table
|
||||
from .Subclasses import KH2Location
|
||||
from .Names import LocationName, RegionName
|
||||
from .Items import Events_Table
|
||||
|
||||
KH2REGIONS: typing.Dict[str, typing.List[str]] = {
|
||||
"Menu": [],
|
||||
@@ -788,7 +790,7 @@ KH2REGIONS: typing.Dict[str, typing.List[str]] = {
|
||||
LocationName.ArmoredXemnas2EventLocation
|
||||
],
|
||||
RegionName.FinalXemnas: [
|
||||
LocationName.FinalXemnas
|
||||
LocationName.FinalXemnasEventLocation
|
||||
],
|
||||
RegionName.DataXemnas: [
|
||||
LocationName.XemnasDataPowerBoost,
|
||||
@@ -1020,7 +1022,8 @@ 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 event_location_to_item.items():
|
||||
|
||||
for location, item in Locations.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
|
||||
from .Items import exclusion_item_table, visit_locking_dict, DonaldAbility_Table, GoofyAbility_Table, SupportAbility_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,6 +83,8 @@ 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:
|
||||
@@ -224,7 +226,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),
|
||||
@@ -266,10 +268,11 @@ 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.FinalXemnas, self.player)
|
||||
final_xemnas_location = self.multiworld.get_location(LocationName.FinalXemnasEventLocation, 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]:
|
||||
@@ -291,8 +294,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:
|
||||
@@ -418,7 +421,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),
|
||||
RegionName.Terra: lambda state: self.get_terra_rules(state) and state.has(ItemName.ProofofConnection, self.player),
|
||||
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(),
|
||||
|
||||
29
worlds/kh2/Subclasses.py
Normal file
29
worlds/kh2/Subclasses.py
Normal file
@@ -0,0 +1,29 @@
|
||||
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,6 +12,7 @@ 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():
|
||||
@@ -49,7 +50,9 @@ 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]
|
||||
@@ -253,11 +256,8 @@ class KH2World(World):
|
||||
self.goofy_gen_early()
|
||||
self.keyblade_gen_early()
|
||||
|
||||
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
|
||||
# final xemnas isn't a location anymore
|
||||
# self.total_locations -= 1
|
||||
|
||||
if self.options.WeaponSlotStartHint:
|
||||
for location in all_weapon_slot:
|
||||
|
||||
@@ -16,6 +16,7 @@ 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
|
||||
@@ -23,7 +24,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 party members' abilities.
|
||||
Every item in the game except for abilities on weapons.
|
||||
|
||||
<h2 style="text-transform:none";>What is The Garden of Assemblage "GoA"?</h2>
|
||||
|
||||
@@ -73,6 +74,8 @@ 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
|
||||
@@ -87,6 +90,7 @@ 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.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>
|
||||
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>
|
||||
|
||||
- Needed for Archipelago
|
||||
1. [`ArchipelagoKH2Client.exe`](https://github.com/ArchipelagoMW/Archipelago/releases)<br>
|
||||
2. `Install mod from JaredWeakStrike/APCompanion`<br>
|
||||
3. `Install mod from KH2FM-Mods-equations19/auto-save`<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>
|
||||
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";>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";>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";>F.A.Q.</h2>
|
||||
|
||||
- Why is my HP/MP continuously increasing without stopping?
|
||||
|
||||
@@ -399,6 +399,26 @@ 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.
|
||||
@@ -444,6 +464,7 @@ 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). Make sure to check the box for `Links Awakening DX`
|
||||
- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases).
|
||||
- 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,11 +10,12 @@
|
||||
|
||||
## Installation Procedures
|
||||
|
||||
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.
|
||||
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..
|
||||
|
||||
2. You should assign your emulator as your default program for launching ROM
|
||||
3. 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](../player-settings) on the website allows you to easily configure your personal settings
|
||||
and export a config file from them.
|
||||
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
|
||||
|
||||
## How-to-play
|
||||
|
||||
|
||||
@@ -1118,7 +1118,13 @@
|
||||
id: Cross Room/Panel_north_missing
|
||||
colors: green
|
||||
tag: forbid
|
||||
required_room: Outside The Bold
|
||||
required_panel:
|
||||
- room: Outside The Bold
|
||||
panel: MOUTH
|
||||
- room: Outside The Bold
|
||||
panel: YEAST
|
||||
- room: Outside The Bold
|
||||
panel: WET
|
||||
DIAMONDS:
|
||||
id: Cross Room/Panel_diamonds_missing
|
||||
colors: green
|
||||
@@ -2635,12 +2641,6 @@
|
||||
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,31 +2651,53 @@
|
||||
id: Countdown Panels/Panel_colorful_colorful
|
||||
check: True
|
||||
tag: forbid
|
||||
required_door:
|
||||
required_panel:
|
||||
- room: The Colorful (White)
|
||||
door: Progress Door
|
||||
panel: BEGIN
|
||||
- room: The Colorful (Black)
|
||||
door: Progress Door
|
||||
panel: FOUND
|
||||
- room: The Colorful (Red)
|
||||
door: Progress Door
|
||||
panel: LOAF
|
||||
- room: The Colorful (Yellow)
|
||||
door: Progress Door
|
||||
panel: CREAM
|
||||
- room: The Colorful (Blue)
|
||||
door: Progress Door
|
||||
panel: SUN
|
||||
- room: The Colorful (Purple)
|
||||
door: Progress Door
|
||||
panel: SPOON
|
||||
- room: The Colorful (Orange)
|
||||
door: Progress Door
|
||||
panel: LETTERS
|
||||
- room: The Colorful (Green)
|
||||
door: Progress Door
|
||||
panel: WALLS
|
||||
- room: The Colorful (Brown)
|
||||
door: Progress Door
|
||||
panel: IRON
|
||||
- room: The Colorful (Gray)
|
||||
door: Progress Door
|
||||
panel: OBSTACLE
|
||||
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:
|
||||
@@ -4202,9 +4224,6 @@
|
||||
SIX:
|
||||
id: Backside Room/Panel_six_six_5
|
||||
tag: midwhite
|
||||
colors:
|
||||
- red
|
||||
- yellow
|
||||
hunt: True
|
||||
required_door:
|
||||
room: Number Hunt
|
||||
@@ -4280,9 +4299,6 @@
|
||||
SIX:
|
||||
id: Backside Room/Panel_six_six_6
|
||||
tag: midwhite
|
||||
colors:
|
||||
- red
|
||||
- yellow
|
||||
hunt: True
|
||||
required_door:
|
||||
room: Number Hunt
|
||||
@@ -4404,9 +4420,14 @@
|
||||
colors: blue
|
||||
tag: forbid
|
||||
required_panel:
|
||||
room: The Bearer (West)
|
||||
panel: SMILE
|
||||
required_room: Outside The Bold
|
||||
- room: The Bearer (West)
|
||||
panel: SMILE
|
||||
- room: Outside The Bold
|
||||
panel: MOUTH
|
||||
- room: Outside The Bold
|
||||
panel: YEAST
|
||||
- room: Outside The Bold
|
||||
panel: WET
|
||||
Cross Tower (South):
|
||||
entrances: # No roof access
|
||||
The Bearer (North):
|
||||
|
||||
@@ -1452,3 +1452,4 @@ progression:
|
||||
Progressive Fearless: 444470
|
||||
Progressive Orange Tower: 444482
|
||||
Progressive Art Gallery: 444563
|
||||
Progressive Colorful: 444580
|
||||
|
||||
@@ -28,6 +28,10 @@ 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":
|
||||
@@ -70,6 +74,8 @@ 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,6 +21,13 @@ 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.
|
||||
@@ -117,6 +124,7 @@ 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,7 +83,8 @@ 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:
|
||||
if (room_name == "Orange Tower" and not world.options.progressive_orange_tower)\
|
||||
or (room_name == "The Colorful" and not world.options.progressive_colorful):
|
||||
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
|
||||
@@ -223,7 +224,7 @@ class LingoPlayerLogic:
|
||||
"kind of logic error.")
|
||||
|
||||
if door_shuffle != ShuffleDoors.option_none and location_classification != LocationClassification.insanity \
|
||||
and not early_color_hallways is False:
|
||||
and not early_color_hallways:
|
||||
# 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). Make sure to check the box for `SNI Client - Lufia II Ancient Cave Patch Setup`
|
||||
- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases).
|
||||
- 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,11 +14,12 @@ modded SNES minis are currently not supported by SNI**
|
||||
|
||||
## Installation Procedures
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
2. If you are using an emulator, you should assign your Lua capable emulator as your default program for launching ROM
|
||||
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.
|
||||
2. Right-click on a ROM file and select **Open with...**
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
- 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,8 +10,7 @@ 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)
|
||||
(select `MegaMan Battle Network 3 Client` during installation).
|
||||
- The built-in Archipelago client, which can be installed [here](https://github.com/ArchipelagoMW/Archipelago/releases).
|
||||
- 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,6 +34,7 @@ class MuseDashCollections:
|
||||
"Rush-Hour",
|
||||
"Find this Month's Featured Playlist",
|
||||
"PeroPero in the Universe",
|
||||
"umpopoff"
|
||||
]
|
||||
|
||||
album_items: Dict[str, AlbumData] = {}
|
||||
@@ -81,11 +82,22 @@ class MuseDashCollections:
|
||||
steamer_mode = sections[3] == "True"
|
||||
|
||||
if song_name in self.DIFF_OVERRIDES:
|
||||
# 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
|
||||
# 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
|
||||
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|
|
||||
Monosugoi Space Shuttle de Koishi ga Monosugoi uta|55-2|Touhou Mugakudan -2-|False|3|5|7|11
|
||||
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,4 +501,12 @@ 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|
|
||||
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|
|
||||
@@ -36,7 +36,7 @@ class AdditionalSongs(Range):
|
||||
- The final song count may be lower due to other settings.
|
||||
"""
|
||||
range_start = 15
|
||||
range_end = 500 # Note will probably not reach this high if any other settings are done.
|
||||
range_end = 508 # Note will probably not reach this high if any other settings are done.
|
||||
default = 40
|
||||
display_name = "Additional Song Count"
|
||||
|
||||
|
||||
@@ -328,5 +328,6 @@ 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
|
||||
"gradeNeeded": self.options.grade_needed.value,
|
||||
"hasFiller": True,
|
||||
}
|
||||
|
||||
@@ -66,5 +66,11 @@ class DifficultyRanges(MuseDashTestBase):
|
||||
for song_name in muse_dash_world.md_collection.DIFF_OVERRIDES:
|
||||
song = muse_dash_world.md_collection.song_items[song_name]
|
||||
|
||||
self.assertTrue(song.easy is not None and song.hard is not None and song.master is not None,
|
||||
# umpopoff is a one time weird song. Its currently the only song in the game
|
||||
# with non-standard difficulties and also doesn't have 3 or more difficulties.
|
||||
if song_name == 'umpopoff':
|
||||
self.assertTrue(song.easy is None and song.hard is not None and song.master is None,
|
||||
f"Song '{song_name}' difficulty not set when it should be.")
|
||||
else:
|
||||
self.assertTrue(song.easy is not None and song.hard is not None and song.master is not None,
|
||||
f"Song '{song_name}' difficulty not set when it should be.")
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
from typing import Dict
|
||||
|
||||
from BaseClasses import Item, ItemClassification, Location, MultiWorld, Region
|
||||
from . import Items, Locations
|
||||
|
||||
|
||||
def create_event(player: int, name: str) -> Item:
|
||||
return Items.NoitaItem(name, ItemClassification.progression, None, player)
|
||||
|
||||
|
||||
def create_location(player: int, name: str, region: Region) -> Location:
|
||||
return Locations.NoitaLocation(player, name, None, region)
|
||||
|
||||
|
||||
def create_locked_location_event(multiworld: MultiWorld, player: int, region_name: str, item: str) -> Location:
|
||||
region = multiworld.get_region(region_name, player)
|
||||
|
||||
new_location = create_location(player, item, region)
|
||||
new_location.place_locked_item(create_event(player, item))
|
||||
|
||||
region.locations.append(new_location)
|
||||
return new_location
|
||||
|
||||
|
||||
def create_all_events(multiworld: MultiWorld, player: int) -> None:
|
||||
for region, event in event_locks.items():
|
||||
create_locked_location_event(multiworld, player, region, event)
|
||||
|
||||
multiworld.completion_condition[player] = lambda state: state.has("Victory", player)
|
||||
|
||||
|
||||
# Maps region names to event names
|
||||
event_locks: Dict[str, str] = {
|
||||
"The Work": "Victory",
|
||||
"Mines": "Portal to Holy Mountain 1",
|
||||
"Coal Pits": "Portal to Holy Mountain 2",
|
||||
"Snowy Depths": "Portal to Holy Mountain 3",
|
||||
"Hiisi Base": "Portal to Holy Mountain 4",
|
||||
"Underground Jungle": "Portal to Holy Mountain 5",
|
||||
"The Vault": "Portal to Holy Mountain 6",
|
||||
"Temple of the Art": "Portal to Holy Mountain 7",
|
||||
}
|
||||
@@ -1,166 +0,0 @@
|
||||
from typing import List, NamedTuple, Set
|
||||
|
||||
from BaseClasses import CollectionState, MultiWorld
|
||||
from . import Items, Locations
|
||||
from .Options import BossesAsChecks, VictoryCondition
|
||||
from worlds.generic import Rules as GenericRules
|
||||
|
||||
|
||||
class EntranceLock(NamedTuple):
|
||||
source: str
|
||||
destination: str
|
||||
event: str
|
||||
items_needed: int
|
||||
|
||||
|
||||
entrance_locks: List[EntranceLock] = [
|
||||
EntranceLock("Mines", "Coal Pits Holy Mountain", "Portal to Holy Mountain 1", 1),
|
||||
EntranceLock("Coal Pits", "Snowy Depths Holy Mountain", "Portal to Holy Mountain 2", 2),
|
||||
EntranceLock("Snowy Depths", "Hiisi Base Holy Mountain", "Portal to Holy Mountain 3", 3),
|
||||
EntranceLock("Hiisi Base", "Underground Jungle Holy Mountain", "Portal to Holy Mountain 4", 4),
|
||||
EntranceLock("Underground Jungle", "Vault Holy Mountain", "Portal to Holy Mountain 5", 5),
|
||||
EntranceLock("The Vault", "Temple of the Art Holy Mountain", "Portal to Holy Mountain 6", 6),
|
||||
EntranceLock("Temple of the Art", "Laboratory Holy Mountain", "Portal to Holy Mountain 7", 7),
|
||||
]
|
||||
|
||||
|
||||
holy_mountain_regions: List[str] = [
|
||||
"Coal Pits Holy Mountain",
|
||||
"Snowy Depths Holy Mountain",
|
||||
"Hiisi Base Holy Mountain",
|
||||
"Underground Jungle Holy Mountain",
|
||||
"Vault Holy Mountain",
|
||||
"Temple of the Art Holy Mountain",
|
||||
"Laboratory Holy Mountain",
|
||||
]
|
||||
|
||||
|
||||
wand_tiers: List[str] = [
|
||||
"Wand (Tier 1)", # Coal Pits
|
||||
"Wand (Tier 2)", # Snowy Depths
|
||||
"Wand (Tier 3)", # Hiisi Base
|
||||
"Wand (Tier 4)", # Underground Jungle
|
||||
"Wand (Tier 5)", # The Vault
|
||||
"Wand (Tier 6)", # Temple of the Art
|
||||
]
|
||||
|
||||
items_hidden_from_shops: List[str] = ["Gold (200)", "Gold (1000)", "Potion", "Random Potion", "Secret Potion",
|
||||
"Chaos Die", "Greed Die", "Kammi", "Refreshing Gourd", "Sädekivi", "Broken Wand",
|
||||
"Powder Pouch"]
|
||||
|
||||
perk_list: List[str] = list(filter(Items.item_is_perk, Items.item_table.keys()))
|
||||
|
||||
|
||||
# ----------------
|
||||
# Helper Functions
|
||||
# ----------------
|
||||
|
||||
|
||||
def has_perk_count(state: CollectionState, player: int, amount: int) -> bool:
|
||||
return sum(state.count(perk, player) for perk in perk_list) >= amount
|
||||
|
||||
|
||||
def has_orb_count(state: CollectionState, player: int, amount: int) -> bool:
|
||||
return state.count("Orb", player) >= amount
|
||||
|
||||
|
||||
def forbid_items_at_location(multiworld: MultiWorld, location_name: str, items: Set[str], player: int):
|
||||
location = multiworld.get_location(location_name, player)
|
||||
GenericRules.forbid_items_for_player(location, items, player)
|
||||
|
||||
|
||||
# ----------------
|
||||
# Rule Functions
|
||||
# ----------------
|
||||
|
||||
|
||||
# Prevent gold and potions from appearing as purchasable items in shops (because physics will destroy them)
|
||||
def ban_items_from_shops(multiworld: MultiWorld, player: int) -> None:
|
||||
for location_name in Locations.location_name_to_id.keys():
|
||||
if "Shop Item" in location_name:
|
||||
forbid_items_at_location(multiworld, location_name, items_hidden_from_shops, player)
|
||||
|
||||
|
||||
# Prevent high tier wands from appearing in early Holy Mountain shops
|
||||
def ban_early_high_tier_wands(multiworld: MultiWorld, player: int) -> None:
|
||||
for i, region_name in enumerate(holy_mountain_regions):
|
||||
wands_to_forbid = wand_tiers[i+1:]
|
||||
|
||||
locations_in_region = Locations.location_region_mapping[region_name].keys()
|
||||
for location_name in locations_in_region:
|
||||
forbid_items_at_location(multiworld, location_name, wands_to_forbid, player)
|
||||
|
||||
# Prevent high tier wands from appearing in the Secret shop
|
||||
wands_to_forbid = wand_tiers[3:]
|
||||
locations_in_region = Locations.location_region_mapping["Secret Shop"].keys()
|
||||
for location_name in locations_in_region:
|
||||
forbid_items_at_location(multiworld, location_name, wands_to_forbid, player)
|
||||
|
||||
|
||||
def lock_holy_mountains_into_spheres(multiworld: MultiWorld, player: int) -> None:
|
||||
for lock in entrance_locks:
|
||||
location = multiworld.get_entrance(f"From {lock.source} To {lock.destination}", player)
|
||||
GenericRules.set_rule(location, lambda state, evt=lock.event: state.has(evt, player))
|
||||
|
||||
|
||||
def holy_mountain_unlock_conditions(multiworld: MultiWorld, player: int) -> None:
|
||||
victory_condition = multiworld.victory_condition[player].value
|
||||
for lock in entrance_locks:
|
||||
location = multiworld.get_location(lock.event, player)
|
||||
|
||||
if victory_condition == VictoryCondition.option_greed_ending:
|
||||
location.access_rule = lambda state, items_needed=lock.items_needed: (
|
||||
has_perk_count(state, player, items_needed//2)
|
||||
)
|
||||
elif victory_condition == VictoryCondition.option_pure_ending:
|
||||
location.access_rule = lambda state, items_needed=lock.items_needed: (
|
||||
has_perk_count(state, player, items_needed//2) and
|
||||
has_orb_count(state, player, items_needed)
|
||||
)
|
||||
elif victory_condition == VictoryCondition.option_peaceful_ending:
|
||||
location.access_rule = lambda state, items_needed=lock.items_needed: (
|
||||
has_perk_count(state, player, items_needed//2) and
|
||||
has_orb_count(state, player, items_needed * 3)
|
||||
)
|
||||
|
||||
|
||||
def biome_unlock_conditions(multiworld: MultiWorld, player: int):
|
||||
lukki_entrances = multiworld.get_region("Lukki Lair", player).entrances
|
||||
magical_entrances = multiworld.get_region("Magical Temple", player).entrances
|
||||
wizard_entrances = multiworld.get_region("Wizards' Den", player).entrances
|
||||
for entrance in lukki_entrances:
|
||||
entrance.access_rule = lambda state: state.has("Melee Immunity Perk", player) and\
|
||||
state.has("All-Seeing Eye Perk", player)
|
||||
for entrance in magical_entrances:
|
||||
entrance.access_rule = lambda state: state.has("All-Seeing Eye Perk", player)
|
||||
for entrance in wizard_entrances:
|
||||
entrance.access_rule = lambda state: state.has("All-Seeing Eye Perk", player)
|
||||
|
||||
|
||||
def victory_unlock_conditions(multiworld: MultiWorld, player: int) -> None:
|
||||
victory_condition = multiworld.victory_condition[player].value
|
||||
victory_location = multiworld.get_location("Victory", player)
|
||||
|
||||
if victory_condition == VictoryCondition.option_pure_ending:
|
||||
victory_location.access_rule = lambda state: has_orb_count(state, player, 11)
|
||||
elif victory_condition == VictoryCondition.option_peaceful_ending:
|
||||
victory_location.access_rule = lambda state: has_orb_count(state, player, 33)
|
||||
|
||||
|
||||
# ----------------
|
||||
# Main Function
|
||||
# ----------------
|
||||
|
||||
|
||||
def create_all_rules(multiworld: MultiWorld, player: int) -> None:
|
||||
if multiworld.players > 1:
|
||||
ban_items_from_shops(multiworld, player)
|
||||
ban_early_high_tier_wands(multiworld, player)
|
||||
lock_holy_mountains_into_spheres(multiworld, player)
|
||||
holy_mountain_unlock_conditions(multiworld, player)
|
||||
biome_unlock_conditions(multiworld, player)
|
||||
victory_unlock_conditions(multiworld, player)
|
||||
|
||||
# Prevent the Map perk (used to find Toveri) from being on Toveri (boss)
|
||||
if multiworld.bosses_as_checks[player].value >= BossesAsChecks.option_all_bosses:
|
||||
forbid_items_at_location(multiworld, "Toveri", {"Spatial Awareness Perk"}, player)
|
||||
@@ -1,6 +1,8 @@
|
||||
from BaseClasses import Item, Tutorial
|
||||
from worlds.AutoWorld import WebWorld, World
|
||||
from . import Events, Items, Locations, Options, Regions, Rules
|
||||
from typing import Dict, Any
|
||||
from . import events, items, locations, regions, rules
|
||||
from .options import NoitaOptions
|
||||
|
||||
|
||||
class NoitaWeb(WebWorld):
|
||||
@@ -24,33 +26,38 @@ class NoitaWorld(World):
|
||||
"""
|
||||
|
||||
game = "Noita"
|
||||
option_definitions = Options.noita_options
|
||||
options: NoitaOptions
|
||||
options_dataclass = NoitaOptions
|
||||
|
||||
item_name_to_id = Items.item_name_to_id
|
||||
location_name_to_id = Locations.location_name_to_id
|
||||
item_name_to_id = items.item_name_to_id
|
||||
location_name_to_id = locations.location_name_to_id
|
||||
|
||||
item_name_groups = Items.item_name_groups
|
||||
location_name_groups = Locations.location_name_groups
|
||||
item_name_groups = items.item_name_groups
|
||||
location_name_groups = locations.location_name_groups
|
||||
data_version = 2
|
||||
|
||||
web = NoitaWeb()
|
||||
|
||||
def generate_early(self):
|
||||
if not self.multiworld.get_player_name(self.player).isascii():
|
||||
raise Exception("Noita yaml's slot name has invalid character(s).")
|
||||
|
||||
# Returned items will be sent over to the client
|
||||
def fill_slot_data(self):
|
||||
return {name: getattr(self.multiworld, name)[self.player].value for name in self.option_definitions}
|
||||
def fill_slot_data(self) -> Dict[str, Any]:
|
||||
return self.options.as_dict("death_link", "victory_condition", "path_option", "hidden_chests",
|
||||
"pedestal_checks", "orbs_as_checks", "bosses_as_checks", "extra_orbs", "shop_price")
|
||||
|
||||
def create_regions(self) -> None:
|
||||
Regions.create_all_regions_and_connections(self.multiworld, self.player)
|
||||
Events.create_all_events(self.multiworld, self.player)
|
||||
regions.create_all_regions_and_connections(self)
|
||||
|
||||
def create_item(self, name: str) -> Item:
|
||||
return Items.create_item(self.player, name)
|
||||
return items.create_item(self.player, name)
|
||||
|
||||
def create_items(self) -> None:
|
||||
Items.create_all_items(self.multiworld, self.player)
|
||||
items.create_all_items(self)
|
||||
|
||||
def set_rules(self) -> None:
|
||||
Rules.create_all_rules(self.multiworld, self.player)
|
||||
rules.create_all_rules(self)
|
||||
|
||||
def get_filler_item_name(self) -> str:
|
||||
return self.multiworld.random.choice(Items.filler_items)
|
||||
return self.random.choice(items.filler_items)
|
||||
|
||||
@@ -40,6 +40,8 @@ or try restarting your game.
|
||||
### What is a YAML and why do I need one?
|
||||
You can see the [basic multiworld setup guide](/tutorial/Archipelago/setup/en) here on the Archipelago website to learn
|
||||
about why Archipelago uses YAML files and what they're for.
|
||||
Please note that Noita only allows you to type certain characters for your slot name.
|
||||
These characters are: `` !#$%&'()+,-.0123456789;=@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmnopqrstuvwxyz{}~<>|\/``
|
||||
|
||||
### Where do I get a YAML?
|
||||
You can use the [game settings page for Noita](/games/Noita/player-settings) here on the Archipelago website to
|
||||
@@ -54,4 +56,4 @@ Place the unzipped pack in the `packs` folder. Then, open Poptracker and open th
|
||||
Click on the "AP" symbol at the top, then enter the desired address, slot name, and password.
|
||||
|
||||
That's all you need for it. It will provide you with a quick reference to see which checks you've done and
|
||||
which checks you still have left.
|
||||
which checks you still have left.
|
||||
|
||||
43
worlds/noita/events.py
Normal file
43
worlds/noita/events.py
Normal file
@@ -0,0 +1,43 @@
|
||||
from typing import Dict, TYPE_CHECKING
|
||||
from BaseClasses import Item, ItemClassification, Location, Region
|
||||
from . import items, locations
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import NoitaWorld
|
||||
|
||||
|
||||
def create_event(player: int, name: str) -> Item:
|
||||
return items.NoitaItem(name, ItemClassification.progression, None, player)
|
||||
|
||||
|
||||
def create_location(player: int, name: str, region: Region) -> Location:
|
||||
return locations.NoitaLocation(player, name, None, region)
|
||||
|
||||
|
||||
def create_locked_location_event(player: int, region: Region, item: str) -> Location:
|
||||
new_location = create_location(player, item, region)
|
||||
new_location.place_locked_item(create_event(player, item))
|
||||
|
||||
region.locations.append(new_location)
|
||||
return new_location
|
||||
|
||||
|
||||
def create_all_events(world: "NoitaWorld", created_regions: Dict[str, Region]) -> None:
|
||||
for region_name, event in event_locks.items():
|
||||
region = created_regions[region_name]
|
||||
create_locked_location_event(world.player, region, event)
|
||||
|
||||
world.multiworld.completion_condition[world.player] = lambda state: state.has("Victory", world.player)
|
||||
|
||||
|
||||
# Maps region names to event names
|
||||
event_locks: Dict[str, str] = {
|
||||
"The Work": "Victory",
|
||||
"Mines": "Portal to Holy Mountain 1",
|
||||
"Coal Pits": "Portal to Holy Mountain 2",
|
||||
"Snowy Depths": "Portal to Holy Mountain 3",
|
||||
"Hiisi Base": "Portal to Holy Mountain 4",
|
||||
"Underground Jungle": "Portal to Holy Mountain 5",
|
||||
"The Vault": "Portal to Holy Mountain 6",
|
||||
"Temple of the Art": "Portal to Holy Mountain 7",
|
||||
}
|
||||
@@ -1,9 +1,14 @@
|
||||
import itertools
|
||||
from collections import Counter
|
||||
from typing import Dict, List, NamedTuple, Set
|
||||
from typing import Dict, List, NamedTuple, Set, TYPE_CHECKING
|
||||
|
||||
from BaseClasses import Item, ItemClassification, MultiWorld
|
||||
from .Options import BossesAsChecks, VictoryCondition, ExtraOrbs
|
||||
from BaseClasses import Item, ItemClassification
|
||||
from .options import BossesAsChecks, VictoryCondition, ExtraOrbs
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import NoitaWorld
|
||||
else:
|
||||
NoitaWorld = object
|
||||
|
||||
|
||||
class ItemData(NamedTuple):
|
||||
@@ -44,39 +49,40 @@ def create_kantele(victory_condition: VictoryCondition) -> List[str]:
|
||||
return ["Kantele"] if victory_condition.value >= VictoryCondition.option_pure_ending else []
|
||||
|
||||
|
||||
def create_random_items(multiworld: MultiWorld, player: int, weights: Dict[str, int], count: int) -> List[str]:
|
||||
def create_random_items(world: NoitaWorld, weights: Dict[str, int], count: int) -> List[str]:
|
||||
filler_pool = weights.copy()
|
||||
if multiworld.bad_effects[player].value == 0:
|
||||
if not world.options.bad_effects:
|
||||
del filler_pool["Trap"]
|
||||
|
||||
return multiworld.random.choices(population=list(filler_pool.keys()),
|
||||
weights=list(filler_pool.values()),
|
||||
k=count)
|
||||
return world.random.choices(population=list(filler_pool.keys()),
|
||||
weights=list(filler_pool.values()),
|
||||
k=count)
|
||||
|
||||
|
||||
def create_all_items(multiworld: MultiWorld, player: int) -> None:
|
||||
locations_to_fill = len(multiworld.get_unfilled_locations(player))
|
||||
def create_all_items(world: NoitaWorld) -> None:
|
||||
player = world.player
|
||||
locations_to_fill = len(world.multiworld.get_unfilled_locations(player))
|
||||
|
||||
itempool = (
|
||||
create_fixed_item_pool()
|
||||
+ create_orb_items(multiworld.victory_condition[player], multiworld.extra_orbs[player])
|
||||
+ create_spatial_awareness_item(multiworld.bosses_as_checks[player])
|
||||
+ create_kantele(multiworld.victory_condition[player])
|
||||
+ create_orb_items(world.options.victory_condition, world.options.extra_orbs)
|
||||
+ create_spatial_awareness_item(world.options.bosses_as_checks)
|
||||
+ create_kantele(world.options.victory_condition)
|
||||
)
|
||||
|
||||
# if there's not enough shop-allowed items in the pool, we can encounter gen issues
|
||||
# 39 is the number of shop-valid items we need to guarantee
|
||||
if len(itempool) < 39:
|
||||
itempool += create_random_items(multiworld, player, shop_only_filler_weights, 39 - len(itempool))
|
||||
itempool += create_random_items(world, shop_only_filler_weights, 39 - len(itempool))
|
||||
# this is so that it passes tests and gens if you have minimal locations and only one player
|
||||
if multiworld.players == 1:
|
||||
for location in multiworld.get_unfilled_locations(player):
|
||||
if world.multiworld.players == 1:
|
||||
for location in world.multiworld.get_unfilled_locations(player):
|
||||
if "Shop Item" in location.name:
|
||||
location.item = create_item(player, itempool.pop())
|
||||
locations_to_fill = len(multiworld.get_unfilled_locations(player))
|
||||
locations_to_fill = len(world.multiworld.get_unfilled_locations(player))
|
||||
|
||||
itempool += create_random_items(multiworld, player, filler_weights, locations_to_fill - len(itempool))
|
||||
multiworld.itempool += [create_item(player, name) for name in itempool]
|
||||
itempool += create_random_items(world, filler_weights, locations_to_fill - len(itempool))
|
||||
world.multiworld.itempool += [create_item(player, name) for name in itempool]
|
||||
|
||||
|
||||
# 110000 - 110032
|
||||
@@ -201,11 +201,10 @@ location_region_mapping: Dict[str, Dict[str, LocationData]] = {
|
||||
}
|
||||
|
||||
|
||||
# Iterating the hidden chest and pedestal locations here to avoid clutter above
|
||||
def generate_location_entries(locname: str, locinfo: LocationData) -> Dict[str, int]:
|
||||
if locinfo.ltype in ["chest", "pedestal"]:
|
||||
return {f"{locname} {i + 1}": locinfo.id + i for i in range(20)}
|
||||
return {locname: locinfo.id}
|
||||
def make_location_range(location_name: str, base_id: int, amt: int) -> Dict[str, int]:
|
||||
if amt == 1:
|
||||
return {location_name: base_id}
|
||||
return {f"{location_name} {i+1}": base_id + i for i in range(amt)}
|
||||
|
||||
|
||||
location_name_groups: Dict[str, Set[str]] = {"shop": set(), "orb": set(), "boss": set(), "chest": set(),
|
||||
@@ -215,9 +214,11 @@ location_name_to_id: Dict[str, int] = {}
|
||||
|
||||
for location_group in location_region_mapping.values():
|
||||
for locname, locinfo in location_group.items():
|
||||
location_name_to_id.update(generate_location_entries(locname, locinfo))
|
||||
if locinfo.ltype in ["chest", "pedestal"]:
|
||||
for i in range(20):
|
||||
location_name_groups[locinfo.ltype].add(f"{locname} {i + 1}")
|
||||
else:
|
||||
location_name_groups[locinfo.ltype].add(locname)
|
||||
# Iterating the hidden chest and pedestal locations here to avoid clutter above
|
||||
amount = 20 if locinfo.ltype in ["chest", "pedestal"] else 1
|
||||
entries = make_location_range(locname, locinfo.id, amount)
|
||||
|
||||
location_name_to_id.update(entries)
|
||||
location_name_groups[locinfo.ltype].update(entries.keys())
|
||||
|
||||
shop_locations = {name for name in location_name_to_id.keys() if "Shop Item" in name}
|
||||
@@ -1,5 +1,5 @@
|
||||
from typing import Dict
|
||||
from Options import AssembleOptions, Choice, DeathLink, DefaultOnToggle, Range, StartInventoryPool
|
||||
from Options import Choice, DeathLink, DefaultOnToggle, Range, StartInventoryPool, PerGameCommonOptions
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
class PathOption(Choice):
|
||||
@@ -99,16 +99,16 @@ class ShopPrice(Choice):
|
||||
default = 100
|
||||
|
||||
|
||||
noita_options: Dict[str, AssembleOptions] = {
|
||||
"start_inventory_from_pool": StartInventoryPool,
|
||||
"death_link": DeathLink,
|
||||
"bad_effects": Traps,
|
||||
"victory_condition": VictoryCondition,
|
||||
"path_option": PathOption,
|
||||
"hidden_chests": HiddenChests,
|
||||
"pedestal_checks": PedestalChecks,
|
||||
"orbs_as_checks": OrbsAsChecks,
|
||||
"bosses_as_checks": BossesAsChecks,
|
||||
"extra_orbs": ExtraOrbs,
|
||||
"shop_price": ShopPrice,
|
||||
}
|
||||
@dataclass
|
||||
class NoitaOptions(PerGameCommonOptions):
|
||||
start_inventory_from_pool: StartInventoryPool
|
||||
death_link: DeathLink
|
||||
bad_effects: Traps
|
||||
victory_condition: VictoryCondition
|
||||
path_option: PathOption
|
||||
hidden_chests: HiddenChests
|
||||
pedestal_checks: PedestalChecks
|
||||
orbs_as_checks: OrbsAsChecks
|
||||
bosses_as_checks: BossesAsChecks
|
||||
extra_orbs: ExtraOrbs
|
||||
shop_price: ShopPrice
|
||||
@@ -1,48 +1,43 @@
|
||||
# Regions are areas in your game that you travel to.
|
||||
from typing import Dict, Set, List
|
||||
from typing import Dict, List, TYPE_CHECKING
|
||||
|
||||
from BaseClasses import Entrance, MultiWorld, Region
|
||||
from . import Locations
|
||||
from BaseClasses import Entrance, Region
|
||||
from . import locations
|
||||
from .events import create_all_events
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import NoitaWorld
|
||||
|
||||
|
||||
def add_location(player: int, loc_name: str, id: int, region: Region) -> None:
|
||||
location = Locations.NoitaLocation(player, loc_name, id, region)
|
||||
region.locations.append(location)
|
||||
|
||||
|
||||
def add_locations(multiworld: MultiWorld, player: int, region: Region) -> None:
|
||||
locations = Locations.location_region_mapping.get(region.name, {})
|
||||
for location_name, location_data in locations.items():
|
||||
def create_locations(world: "NoitaWorld", region: Region) -> None:
|
||||
locs = locations.location_region_mapping.get(region.name, {})
|
||||
for location_name, location_data in locs.items():
|
||||
location_type = location_data.ltype
|
||||
flag = location_data.flag
|
||||
|
||||
opt_orbs = multiworld.orbs_as_checks[player].value
|
||||
opt_bosses = multiworld.bosses_as_checks[player].value
|
||||
opt_paths = multiworld.path_option[player].value
|
||||
opt_num_chests = multiworld.hidden_chests[player].value
|
||||
opt_num_pedestals = multiworld.pedestal_checks[player].value
|
||||
is_orb_allowed = location_type == "orb" and flag <= world.options.orbs_as_checks
|
||||
is_boss_allowed = location_type == "boss" and flag <= world.options.bosses_as_checks
|
||||
amount = 0
|
||||
if flag == locations.LocationFlag.none or is_orb_allowed or is_boss_allowed:
|
||||
amount = 1
|
||||
elif location_type == "chest" and flag <= world.options.path_option:
|
||||
amount = world.options.hidden_chests.value
|
||||
elif location_type == "pedestal" and flag <= world.options.path_option:
|
||||
amount = world.options.pedestal_checks.value
|
||||
|
||||
is_orb_allowed = location_type == "orb" and flag <= opt_orbs
|
||||
is_boss_allowed = location_type == "boss" and flag <= opt_bosses
|
||||
if flag == Locations.LocationFlag.none or is_orb_allowed or is_boss_allowed:
|
||||
add_location(player, location_name, location_data.id, region)
|
||||
elif location_type == "chest" and flag <= opt_paths:
|
||||
for i in range(opt_num_chests):
|
||||
add_location(player, f"{location_name} {i+1}", location_data.id + i, region)
|
||||
elif location_type == "pedestal" and flag <= opt_paths:
|
||||
for i in range(opt_num_pedestals):
|
||||
add_location(player, f"{location_name} {i+1}", location_data.id + i, region)
|
||||
region.add_locations(locations.make_location_range(location_name, location_data.id, amount),
|
||||
locations.NoitaLocation)
|
||||
|
||||
|
||||
# Creates a new Region with the locations found in `location_region_mapping` and adds them to the world.
|
||||
def create_region(multiworld: MultiWorld, player: int, region_name: str) -> Region:
|
||||
new_region = Region(region_name, player, multiworld)
|
||||
add_locations(multiworld, player, new_region)
|
||||
def create_region(world: "NoitaWorld", region_name: str) -> Region:
|
||||
new_region = Region(region_name, world.player, world.multiworld)
|
||||
create_locations(world, new_region)
|
||||
return new_region
|
||||
|
||||
|
||||
def create_regions(multiworld: MultiWorld, player: int) -> Dict[str, Region]:
|
||||
return {name: create_region(multiworld, player, name) for name in noita_regions}
|
||||
def create_regions(world: "NoitaWorld") -> Dict[str, Region]:
|
||||
return {name: create_region(world, name) for name in noita_regions}
|
||||
|
||||
|
||||
# An "Entrance" is really just a connection between two regions
|
||||
@@ -60,11 +55,12 @@ def create_connections(player: int, regions: Dict[str, Region]) -> None:
|
||||
|
||||
|
||||
# Creates all regions and connections. Called from NoitaWorld.
|
||||
def create_all_regions_and_connections(multiworld: MultiWorld, player: int) -> None:
|
||||
created_regions = create_regions(multiworld, player)
|
||||
create_connections(player, created_regions)
|
||||
def create_all_regions_and_connections(world: "NoitaWorld") -> None:
|
||||
created_regions = create_regions(world)
|
||||
create_connections(world.player, created_regions)
|
||||
create_all_events(world, created_regions)
|
||||
|
||||
multiworld.regions += created_regions.values()
|
||||
world.multiworld.regions += created_regions.values()
|
||||
|
||||
|
||||
# Oh, what a tangled web we weave
|
||||
172
worlds/noita/rules.py
Normal file
172
worlds/noita/rules.py
Normal file
@@ -0,0 +1,172 @@
|
||||
from typing import List, NamedTuple, Set, TYPE_CHECKING
|
||||
|
||||
from BaseClasses import CollectionState
|
||||
from . import items, locations
|
||||
from .options import BossesAsChecks, VictoryCondition
|
||||
from worlds.generic import Rules as GenericRules
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import NoitaWorld
|
||||
|
||||
|
||||
class EntranceLock(NamedTuple):
|
||||
source: str
|
||||
destination: str
|
||||
event: str
|
||||
items_needed: int
|
||||
|
||||
|
||||
entrance_locks: List[EntranceLock] = [
|
||||
EntranceLock("Mines", "Coal Pits Holy Mountain", "Portal to Holy Mountain 1", 1),
|
||||
EntranceLock("Coal Pits", "Snowy Depths Holy Mountain", "Portal to Holy Mountain 2", 2),
|
||||
EntranceLock("Snowy Depths", "Hiisi Base Holy Mountain", "Portal to Holy Mountain 3", 3),
|
||||
EntranceLock("Hiisi Base", "Underground Jungle Holy Mountain", "Portal to Holy Mountain 4", 4),
|
||||
EntranceLock("Underground Jungle", "Vault Holy Mountain", "Portal to Holy Mountain 5", 5),
|
||||
EntranceLock("The Vault", "Temple of the Art Holy Mountain", "Portal to Holy Mountain 6", 6),
|
||||
EntranceLock("Temple of the Art", "Laboratory Holy Mountain", "Portal to Holy Mountain 7", 7),
|
||||
]
|
||||
|
||||
|
||||
holy_mountain_regions: List[str] = [
|
||||
"Coal Pits Holy Mountain",
|
||||
"Snowy Depths Holy Mountain",
|
||||
"Hiisi Base Holy Mountain",
|
||||
"Underground Jungle Holy Mountain",
|
||||
"Vault Holy Mountain",
|
||||
"Temple of the Art Holy Mountain",
|
||||
"Laboratory Holy Mountain",
|
||||
]
|
||||
|
||||
|
||||
wand_tiers: List[str] = [
|
||||
"Wand (Tier 1)", # Coal Pits
|
||||
"Wand (Tier 2)", # Snowy Depths
|
||||
"Wand (Tier 3)", # Hiisi Base
|
||||
"Wand (Tier 4)", # Underground Jungle
|
||||
"Wand (Tier 5)", # The Vault
|
||||
"Wand (Tier 6)", # Temple of the Art
|
||||
]
|
||||
|
||||
|
||||
items_hidden_from_shops: Set[str] = {"Gold (200)", "Gold (1000)", "Potion", "Random Potion", "Secret Potion",
|
||||
"Chaos Die", "Greed Die", "Kammi", "Refreshing Gourd", "Sädekivi", "Broken Wand",
|
||||
"Powder Pouch"}
|
||||
|
||||
perk_list: List[str] = list(filter(items.item_is_perk, items.item_table.keys()))
|
||||
|
||||
|
||||
# ----------------
|
||||
# Helper Functions
|
||||
# ----------------
|
||||
|
||||
|
||||
def has_perk_count(state: CollectionState, player: int, amount: int) -> bool:
|
||||
return sum(state.count(perk, player) for perk in perk_list) >= amount
|
||||
|
||||
|
||||
def has_orb_count(state: CollectionState, player: int, amount: int) -> bool:
|
||||
return state.count("Orb", player) >= amount
|
||||
|
||||
|
||||
def forbid_items_at_locations(world: "NoitaWorld", shop_locations: Set[str], forbidden_items: Set[str]):
|
||||
for shop_location in shop_locations:
|
||||
location = world.multiworld.get_location(shop_location, world.player)
|
||||
GenericRules.forbid_items_for_player(location, forbidden_items, world.player)
|
||||
|
||||
|
||||
# ----------------
|
||||
# Rule Functions
|
||||
# ----------------
|
||||
|
||||
|
||||
# Prevent gold and potions from appearing as purchasable items in shops (because physics will destroy them)
|
||||
# def ban_items_from_shops(world: "NoitaWorld") -> None:
|
||||
# for location_name in Locations.location_name_to_id.keys():
|
||||
# if "Shop Item" in location_name:
|
||||
# forbid_items_at_location(world, location_name, items_hidden_from_shops)
|
||||
def ban_items_from_shops(world: "NoitaWorld") -> None:
|
||||
forbid_items_at_locations(world, locations.shop_locations, items_hidden_from_shops)
|
||||
|
||||
|
||||
# Prevent high tier wands from appearing in early Holy Mountain shops
|
||||
def ban_early_high_tier_wands(world: "NoitaWorld") -> None:
|
||||
for i, region_name in enumerate(holy_mountain_regions):
|
||||
wands_to_forbid = set(wand_tiers[i+1:])
|
||||
|
||||
locations_in_region = set(locations.location_region_mapping[region_name].keys())
|
||||
forbid_items_at_locations(world, locations_in_region, wands_to_forbid)
|
||||
|
||||
# Prevent high tier wands from appearing in the Secret shop
|
||||
wands_to_forbid = set(wand_tiers[3:])
|
||||
locations_in_region = set(locations.location_region_mapping["Secret Shop"].keys())
|
||||
forbid_items_at_locations(world, locations_in_region, wands_to_forbid)
|
||||
|
||||
|
||||
def lock_holy_mountains_into_spheres(world: "NoitaWorld") -> None:
|
||||
for lock in entrance_locks:
|
||||
location = world.multiworld.get_entrance(f"From {lock.source} To {lock.destination}", world.player)
|
||||
GenericRules.set_rule(location, lambda state, evt=lock.event: state.has(evt, world.player))
|
||||
|
||||
|
||||
def holy_mountain_unlock_conditions(world: "NoitaWorld") -> None:
|
||||
victory_condition = world.options.victory_condition.value
|
||||
for lock in entrance_locks:
|
||||
location = world.multiworld.get_location(lock.event, world.player)
|
||||
|
||||
if victory_condition == VictoryCondition.option_greed_ending:
|
||||
location.access_rule = lambda state, items_needed=lock.items_needed: (
|
||||
has_perk_count(state, world.player, items_needed//2)
|
||||
)
|
||||
elif victory_condition == VictoryCondition.option_pure_ending:
|
||||
location.access_rule = lambda state, items_needed=lock.items_needed: (
|
||||
has_perk_count(state, world.player, items_needed//2) and
|
||||
has_orb_count(state, world.player, items_needed)
|
||||
)
|
||||
elif victory_condition == VictoryCondition.option_peaceful_ending:
|
||||
location.access_rule = lambda state, items_needed=lock.items_needed: (
|
||||
has_perk_count(state, world.player, items_needed//2) and
|
||||
has_orb_count(state, world.player, items_needed * 3)
|
||||
)
|
||||
|
||||
|
||||
def biome_unlock_conditions(world: "NoitaWorld"):
|
||||
lukki_entrances = world.multiworld.get_region("Lukki Lair", world.player).entrances
|
||||
magical_entrances = world.multiworld.get_region("Magical Temple", world.player).entrances
|
||||
wizard_entrances = world.multiworld.get_region("Wizards' Den", world.player).entrances
|
||||
for entrance in lukki_entrances:
|
||||
entrance.access_rule = lambda state: state.has("Melee Immunity Perk", world.player) and\
|
||||
state.has("All-Seeing Eye Perk", world.player)
|
||||
for entrance in magical_entrances:
|
||||
entrance.access_rule = lambda state: state.has("All-Seeing Eye Perk", world.player)
|
||||
for entrance in wizard_entrances:
|
||||
entrance.access_rule = lambda state: state.has("All-Seeing Eye Perk", world.player)
|
||||
|
||||
|
||||
def victory_unlock_conditions(world: "NoitaWorld") -> None:
|
||||
victory_condition = world.options.victory_condition.value
|
||||
victory_location = world.multiworld.get_location("Victory", world.player)
|
||||
|
||||
if victory_condition == VictoryCondition.option_pure_ending:
|
||||
victory_location.access_rule = lambda state: has_orb_count(state, world.player, 11)
|
||||
elif victory_condition == VictoryCondition.option_peaceful_ending:
|
||||
victory_location.access_rule = lambda state: has_orb_count(state, world.player, 33)
|
||||
|
||||
|
||||
# ----------------
|
||||
# Main Function
|
||||
# ----------------
|
||||
|
||||
|
||||
def create_all_rules(world: "NoitaWorld") -> None:
|
||||
if world.multiworld.players > 1:
|
||||
ban_items_from_shops(world)
|
||||
ban_early_high_tier_wands(world)
|
||||
lock_holy_mountains_into_spheres(world)
|
||||
holy_mountain_unlock_conditions(world)
|
||||
biome_unlock_conditions(world)
|
||||
victory_unlock_conditions(world)
|
||||
|
||||
# Prevent the Map perk (used to find Toveri) from being on Toveri (boss)
|
||||
if world.options.bosses_as_checks.value >= BossesAsChecks.option_all_bosses:
|
||||
toveri = world.multiworld.get_location("Toveri", world.player)
|
||||
GenericRules.forbid_items_for_player(toveri, {"Spatial Awareness Perk"}, world.player)
|
||||
@@ -1271,7 +1271,7 @@ class LogicTricks(OptionList):
|
||||
https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/oot/LogicTricks.py
|
||||
"""
|
||||
display_name = "Logic Tricks"
|
||||
valid_keys = frozenset(normalized_name_tricks)
|
||||
valid_keys = tuple(normalized_name_tricks.keys())
|
||||
valid_keys_casefold = True
|
||||
|
||||
|
||||
|
||||
@@ -118,7 +118,16 @@ class OOTWeb(WebWorld):
|
||||
["TheLynk"]
|
||||
)
|
||||
|
||||
tutorials = [setup, setup_es, setup_fr]
|
||||
setup_de = Tutorial(
|
||||
setup.tutorial_name,
|
||||
setup.description,
|
||||
"Deutsch",
|
||||
"setup_de.md",
|
||||
"setup/de",
|
||||
["Held_der_Zeit"]
|
||||
)
|
||||
|
||||
tutorials = [setup, setup_es, setup_fr, setup_de]
|
||||
|
||||
|
||||
class OOTWorld(World):
|
||||
|
||||
BIN
worlds/oot/docs/MultiWorld-Room_oot.png
Normal file
BIN
worlds/oot/docs/MultiWorld-Room_oot.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 56 KiB |
41
worlds/oot/docs/de_Ocarina of Time.md
Normal file
41
worlds/oot/docs/de_Ocarina of Time.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# The Legend of Zelda: Ocarina of Time
|
||||
|
||||
## 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.
|
||||
|
||||
## Was macht der Randomizer in diesem Spiel?
|
||||
|
||||
Items, welche der Spieler für gewöhnlich im Verlauf des Spiels erhalten würde, wurden umhergemischt. Die Logik bleit
|
||||
bestehen, damit ist das Spiel immer durchspielbar. Doch weil die Items durch das ganze Spiel gemischt wurden, müssen
|
||||
manche Bereiche früher bescuht werden, als man es in Vanilla tun würde.
|
||||
Eine Liste von implementierter Logik, die unoffensichtlich erscheinen kann, kann
|
||||
[hier (Englisch)](https://wiki.ootrandomizer.com/index.php?title=Logic) gefunden werden.
|
||||
|
||||
## Welche Items und Bereiche werden gemischt?
|
||||
|
||||
Alle ausrüstbare und sammelbare Gegenstände, sowie Munition können gemischt werden. Und alle Bereiche, die einen
|
||||
dieser Items enthalten könnten, haben (sehr wahrscheinlich) ihren Inhalt verändert. Goldene Skulltulas können ebenfalls
|
||||
dazugezählt werden, je nach Wunsch des Spielers.
|
||||
|
||||
## Welche Items können in sich in der Welt eines anderen Spielers befinden?
|
||||
|
||||
Jedes dieser Items, die gemicht werden können, können in einer Multiworld auch in der Welt eines anderen Spielers
|
||||
fallen. Es ist jedoch möglich ausgewählte Items auf deine eigene Welt zu beschränken.
|
||||
|
||||
## Wie sieht ein Item einer anderen Welt in OoT aus?
|
||||
|
||||
Items, die zu einer anderen Welt gehören, werden repräsentiert durch Zelda's Brief.
|
||||
|
||||
## Was passiert, wenn der Spieler ein Item erhält?
|
||||
|
||||
Sobald der Spieler ein Item erhält, wird Link das Item über seinen Kopf halten und der ganzen Welt präsentieren.
|
||||
Gut für's Geschäft!
|
||||
|
||||
## Einzigartige Lokale Befehle
|
||||
|
||||
Die folgenden Befehle stehen nur im OoTClient, um mit Archipelago zu spielen, zur Verfügung:
|
||||
|
||||
- `/n64` Überprüffe den Verbindungsstatus deiner N64
|
||||
- `/deathlink` Schalte den "Deathlink" des Clients um. Überschreibt die zuvor konfigurierten Einstellungen.
|
||||
108
worlds/oot/docs/setup_de.md
Normal file
108
worlds/oot/docs/setup_de.md
Normal file
@@ -0,0 +1,108 @@
|
||||
# Setup Anleitung für Ocarina of Time: Archipelago Edition
|
||||
|
||||
## WICHTIG
|
||||
|
||||
Da wir BizHawk benutzen, gilt diese Anleitung nur für Windows und Linux.
|
||||
|
||||
## Benötigte Software
|
||||
|
||||
- BizHawk: [BizHawk Veröffentlichungen von TASVideos](https://tasvideos.org/BizHawk/ReleaseHistory)
|
||||
- Version 2.3.1 und später werden unterstützt. Version 2.9 ist empfohlen.
|
||||
- Detailierte Installtionsanweisungen für BizHawk können über den obrigen Link gefunden werden.
|
||||
- Windows-Benutzer müssen die Prerequisiten installiert haben. Diese können ebenfalls über
|
||||
den obrigen Link gefunden werden.
|
||||
- Der integrierte Archipelago-Client, welcher [hier](https://github.com/ArchipelagoMW/Archipelago/releases) installiert
|
||||
werden kann.
|
||||
- Eine `Ocarina of Time v1.0 US(?) ROM`. (Nicht aus Europa und keine Master-Quest oder Debug-Rom!)
|
||||
|
||||
## Konfigurieren von BizHawk
|
||||
|
||||
Sobald Bizhawk einmal installiert wurde, öffne **EmuHawk** und ändere die folgenen Einsteluungen:
|
||||
|
||||
- (≤ 2.8) Gehe zu `Config > Customize`. Wechlse zu dem `Advanced`-Reiter, wechsle dann den `Lua Core` von "NLua+KopiLua" zu
|
||||
`"Lua+LuaInterface"`. Starte danach EmuHawk neu. Dies ist zwingend notwendig, damit die Lua-Scripts, mit denen man sich mit dem Client verbindet, ordnungsgemäß funktionieren.
|
||||
**ANMERKUNG: Selbst wenn "Lua+LuaInterface" bereits ausgewählt ist, wechsle zwischen den beiden Optionen umher und**
|
||||
**wähle es erneut aus. Neue Installationen oder Versionen von EmuHawk neigen dazu "Lua+LuaInterface" als die**
|
||||
**Standard-Option anzuzeigen, aber laden dennoch "NLua+KopiLua", bis dieser Schritt getan ist.**
|
||||
- Unter `Config > Customize > Advanced`, gehe sicher dass der Haken bei `AutoSaveRAM` ausgeählt ist, und klicke dann
|
||||
den 5s-Knopf. Dies verringert die Wahrscheinlichkeit den Speicherfrotschritt zu verlieren, sollte der Emulator mal
|
||||
abstürzen.
|
||||
- **(Optional)** Unter `Config > Customize` kannst du die Haken in den "Run in background"
|
||||
(Laufe weiter im Hintergrund) und "Accept background input" (akzeptiere Tastendruck im Hintergrund) Kästchen setzen.
|
||||
Dies erlaubt dir das Spiel im Hintergrund weiter zu spielen, selbst wenn ein anderes Fenster aktiv ist. (Nützlich bei
|
||||
mehreren oder eher großen Bildschrimen/Monitoren.)
|
||||
- Unter `Config > Hotkeys` sind viele Hotkeys, die mit oft genuten Tasten belegt worden sind. Es wird empfohlen die
|
||||
meisten (oder alle) Hotkeys zu deaktivieren. Dies kann schnell mit `Esc` erledigt werden.
|
||||
- Wird mit einem Kontroller gespielt, bei der Tastenbelegung (bei einem Laufendem Spiel, unter
|
||||
`Config > Controllers...`), deaktiviere "P1 A Up", "P1 A Down", "P1 A Left", and "P1 A Right" und gehe stattdessen in
|
||||
den Reiter `Analog Controls` um den Stick zu belegen, da sonst Probleme beim Zielen auftreten (mit dem Bogen oder
|
||||
ähnliches). Y-Axis ist für Oben und Unten, und die X-Axis ist für Links und Rechts.
|
||||
- Unter `N64` setze einen Haken bei "Use Expansion Slot" (Benutze Erweiterungs-Slot). Dies wird benötigt damit
|
||||
savestates/schnellspeichern funktioniert. (Das N64-Menü taucht nur **nach** dem laden einer N64-ROM auf.)
|
||||
|
||||
Es wird sehr empfohlen N64 Rom-Erweiterungen (\*.n64, \*.z64) mit dem Emuhawk - welcher zuvor installiert wurde - zu
|
||||
verknüpfen.
|
||||
Um dies zu tun, muss eine beliebige N64 Rom aufgefunden werden, welche in deinem Besitz ist, diese Rechtsklicken und
|
||||
dann auf "Öffnen mit..." gehen. Gehe dann auf "Andere App auswählen" und suche nach deinen BizHawk-Ordner, in der
|
||||
sich der Emulator befindet, und wähle dann `EmuHawk.exe` **(NICHT "DiscoHawk.exe"!)** aus.
|
||||
|
||||
Eine Alternative BizHawk Setup Anleitung (auf Englisch), sowie weitere Hilfe bei Problemen kann
|
||||
[hier](https://wiki.ootrandomizer.com/index.php?title=Bizhawk) gefunden werden.
|
||||
|
||||
## Erstelle eine YAML-Datei
|
||||
|
||||
### Was ist eine YAML-Datei und Warum brauch ich eine?
|
||||
|
||||
Eine YAML-Datie enthält einen Satz an einstellbaren Optionen, die dem Generator mitteilen, wie
|
||||
dein Spiel generiert werden soll. In einer Multiworld stellt jeder Spieler eine eigene YAML-Datei zur Verfügung. Dies
|
||||
erlaubt jeden Spieler eine personalisierte Erfahrung nach derem Geschmack. Damit kann auch jeder Spieler in einer
|
||||
Multiworld (des gleichen Spiels) völlig unterschiedliche Einstellungen haben.
|
||||
|
||||
Für weitere Informationen, besuche die allgemeine Anleitung zum Erstellen einer
|
||||
YAML-Datei: [Archipelago Setup Anleitung](/tutorial/Archipelago/setup/en)
|
||||
|
||||
### Woher bekomme ich eine YAML-Datei?
|
||||
|
||||
Die Seite für die Spielereinstellungen auf dieser Website erlaubt es dir deine persönlichen Einstellungen nach
|
||||
vorlieben zu konfigurieren und eine YAML-Datei zu exportieren.
|
||||
Seite für die Spielereinstellungen:
|
||||
[Seite für die Spielereinstellungen von Ocarina of Time](/games/Ocarina%20of%20Time/player-options)
|
||||
|
||||
### Überprüfen deiner YAML-Datei
|
||||
|
||||
Wenn du deine YAML-Datei überprüfen möchtest, um sicher zu gehen, dass sie funktioniert, kannst du dies auf der
|
||||
YAML-Überprüfungsseite tun.
|
||||
YAML-Überprüfungsseite: [YAML-Überprüfungsseite](/check)
|
||||
|
||||
## Beitreten einer Multiworld
|
||||
|
||||
### Erhalte deinen OoT-Patch
|
||||
|
||||
(Der folgende Prozess ist bei den meisten ROM-basierenden Spielen sehr ähnlich.)
|
||||
|
||||
Wenn du einer Multiworld beitrittst, wirst du gefordert eine YAML-Datei bei dem Host abzugeben. Ist dies getan,
|
||||
erhälst du (in der Regel) einen Link vom Host der Multiworld. Dieser führt dich zu einem Raum, in dem alle
|
||||
teilnehmenden Spieler (bzw. Welten) aufgelistet sind. Du solltest dich dann auf **deine** Welt konzentrieren
|
||||
und klicke dann auf `Download APZ5 File...`.
|
||||

|
||||
|
||||
Führe die `.apz5`-Datei mit einem Doppelklick aus, um deinen Ocarina Of Time-Client zu starten, sowie das patchen
|
||||
deiner ROM. Ist dieser Prozess fertig (kann etwas dauern), startet sich der Client und der Emulator automatisch
|
||||
(sofern das "Öffnen mit..." ausgewählt wurde).
|
||||
|
||||
### Verbinde zum Multiserver
|
||||
|
||||
Sind einmal der Client und der Emulator gestartet, müssen sie nur noch miteinander verbunden werden. Gehe dazu in
|
||||
deinen Archipelago-Ordner, dann zu `data/lua`, und füge das `connector_oot.lua` Script per Drag&Drop (ziehen und
|
||||
fallen lassen) auf das EmuHawk-Fenster. (Alternativ kannst du die Lua-Konsole manuell öffnen, gehe dazu auf
|
||||
`Script > Open Script` und durchsuche die Ordner nach `data/lua/connector_oot.lua`.)
|
||||
|
||||
Um den Client mit dem Multiserver zu verbinden, füge einfach `<Adresse>:<Port>` in das Textfeld ganz oben im
|
||||
Client ein und drücke Enter oder "Connect" (verbinden). Wird ein Passwort benötigt, musst du es danach unten in das
|
||||
Textfeld (für den Chat und Befehle) eingeben.
|
||||
Alternativ kannst du auch in dem unterem Textfeld den folgenden Befehl schreiben:
|
||||
`/connect <Adresse>:<Port> [Passwort]` (wie die Adresse und der Port lautet steht in dem Raum, oder wird von deinem
|
||||
Host an dich weitergegeben.)
|
||||
Beispiel: `/connect archipelago.gg:12345 Passw123`
|
||||
|
||||
Du bist nun bereit für dein Zeitreise-Abenteuer in Hyrule.
|
||||
@@ -10,8 +10,7 @@ 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)
|
||||
(select `Ocarina of Time Client` during installation).
|
||||
- The built-in Archipelago client, which can be installed [here](https://github.com/ArchipelagoMW/Archipelago/releases).
|
||||
- An Ocarina of Time v1.0 ROM.
|
||||
|
||||
## Configuring BizHawk
|
||||
|
||||
@@ -1,422 +1,70 @@
|
||||
# Guide d'installation Archipelago pour Ocarina of Time
|
||||
# Guide de configuration pour Ocarina of Time Archipelago
|
||||
|
||||
## Important
|
||||
|
||||
Comme nous utilisons BizHawk, ce guide ne s'applique qu'aux systèmes Windows et Linux.
|
||||
Comme nous utilisons BizHawk, ce guide s'applique uniquement aux systèmes Windows et Linux.
|
||||
|
||||
## Logiciel requis
|
||||
|
||||
- BizHawk : [BizHawk sort de TASVideos] (https://tasvideos.org/BizHawk/ReleaseHistory)
|
||||
- Les versions 2.3.1 et ultérieures sont prises en charge. La version 2.7 est recommandée pour la stabilité.
|
||||
- BizHawk : [Sorties BizHawk de TASVideos](https://tasvideos.org/BizHawk/ReleaseHistory)
|
||||
- Les versions 2.3.1 et ultérieures sont prises en charge. La version 2.7 est recommandée pour des raisons de stabilité.
|
||||
- Des instructions d'installation détaillées pour BizHawk peuvent être trouvées sur le lien ci-dessus.
|
||||
- Les utilisateurs Windows doivent d'abord exécuter le programme d'installation prereq, qui peut également être trouvé sur le lien ci-dessus.
|
||||
- Les utilisateurs Windows doivent d'abord exécuter le programme d'installation des prérequis, qui peut également être trouvé sur le lien ci-dessus.
|
||||
- Le client Archipelago intégré, qui peut être installé [ici](https://github.com/ArchipelagoMW/Archipelago/releases)
|
||||
(sélectionnez `Ocarina of Time Client` lors de l'installation).
|
||||
(sélectionnez « Ocarina of Time Client » lors de l'installation).
|
||||
- Une ROM Ocarina of Time v1.0.
|
||||
|
||||
## Configuration de BizHawk
|
||||
|
||||
Une fois BizHawk installé, ouvrez BizHawk et modifiez les paramètres suivants :
|
||||
Une fois BizHawk installé, ouvrez EmuHawk et modifiez les paramètres suivants :
|
||||
|
||||
- Allez dans Config > Personnaliser. Basculez vers l'onglet Avancé, puis basculez le Lua Core de "NLua+KopiLua" vers
|
||||
"Interface Lua+Lua". Redémarrez ensuite BizHawk. Ceci est nécessaire pour que le script Lua fonctionne correctement.
|
||||
**REMARQUE : Même si "Lua+LuaInterface" est déjà sélectionné, basculez entre les deux options et resélectionnez-le. Nouvelles installations**
|
||||
** des versions plus récentes de BizHawk ont tendance à afficher "Lua+LuaInterface" comme option sélectionnée par défaut mais se chargent toujours **
|
||||
**"NLua+KopiLua" jusqu'à ce que cette étape soit terminée.**
|
||||
- Sous Config > Personnaliser > Avancé, assurez-vous que la case pour AutoSaveRAM est cochée et cliquez sur le bouton 5s.
|
||||
Cela réduit la possibilité de perdre des données de sauvegarde en cas de plantage de l'émulateur.
|
||||
- Sous Config > Personnaliser, cochez les cases "Exécuter en arrière-plan" et "Accepter la saisie en arrière-plan". Cela vous permettra de
|
||||
continuer à jouer en arrière-plan, même si une autre fenêtre est sélectionnée.
|
||||
- Sous Config> Raccourcis clavier, de nombreux raccourcis clavier sont répertoriés, dont beaucoup sont liés aux touches communes du clavier. Vous voudrez probablement
|
||||
désactiver la plupart d'entre eux, ce que vous pouvez faire rapidement en utilisant `Esc`.
|
||||
- Si vous jouez avec une manette, lorsque vous liez les commandes, désactivez "P1 A Up", "P1 A Down", "P1 A Left" et "P1 A Right"
|
||||
car ceux-ci interfèrent avec la visée s'ils sont liés. Définissez l'entrée directionnelle à l'aide de l'onglet Analogique à la place.
|
||||
- Sous N64, activez "Utiliser l'emplacement d'extension". Ceci est nécessaire pour que les sauvegardes fonctionnent.
|
||||
- (≤ 2,8) Allez dans Config > Personnaliser. Passez à l'onglet Avancé, puis faites passer le Lua Core de "NLua+KopiLua" à
|
||||
"Lua+LuaInterface". Puis redémarrez EmuHawk. Ceci est nécessaire pour que le script Lua fonctionne correctement.
|
||||
**REMARQUE : Même si « Lua+LuaInterface » est déjà sélectionné, basculez entre les deux options et resélectionnez-la. Nouvelles installations**
|
||||
**des versions plus récentes d'EmuHawk ont tendance à afficher "Lua+LuaInterface" comme option sélectionnée par défaut mais ce pendant refait l'épate juste au dessus par précautions**
|
||||
- Sous Config > Personnaliser > Avancé, assurez-vous que la case AutoSaveRAM est cochée et cliquez sur le bouton 5s.
|
||||
Cela réduit la possibilité de perdre des données de sauvegarde en cas de crash de l'émulateur.
|
||||
- Sous Config > Personnaliser, cochez les cases « Exécuter en arrière-plan » et « Accepter la saisie en arrière-plan ». Cela vous permettra continuez à jouer en arrière-plan, même si une autre fenêtre est sélectionnée.
|
||||
- Sous Config > Hotkeys, de nombreux raccourcis clavier sont répertoriés, dont beaucoup sont liés aux touches communes du clavier. Vous voudrez probablement pour désactiver la plupart d'entre eux, ce que vous pouvez faire rapidement en utilisant « Esc ».
|
||||
- Si vous jouez avec une manette, lorsque vous associez des commandes, désactivez "P1 A Up", "P1 A Down", "P1 A Left" et "P1 A Right".
|
||||
car ceux-ci interfèrent avec la visée s’ils sont liés. Définissez plutôt l'entrée directionnelle à l'aide de l'onglet Analogique.
|
||||
- Sous N64, activez "Utiliser le connecteur d'extension". Ceci est nécessaire pour que les états de sauvegarde fonctionnent.
|
||||
(Le menu N64 n'apparaît qu'après le chargement d'une ROM.)
|
||||
|
||||
Il est fortement recommandé d'associer les extensions de rom N64 (\*.n64, \*.z64) au BizHawk que nous venons d'installer.
|
||||
Pour ce faire, nous devons simplement rechercher n'importe quelle rom N64 que nous possédons, faire un clic droit et sélectionner "Ouvrir avec ...", dépliez
|
||||
la liste qui apparaît et sélectionnez l'option du bas "Rechercher une autre application", puis naviguez jusqu'au dossier BizHawk
|
||||
et sélectionnez EmuHawk.exe.
|
||||
Il est fortement recommandé d'associer les extensions de rom N64 (\*.n64, \*.z64) à l'EmuHawk que nous venons d'installer.
|
||||
Pour ce faire, vous devez simplement rechercher n'importe quelle rom N64 que vous possédez, faire un clic droit et sélectionner "Ouvrir avec...", déplier la liste qui apparaît et sélectionnez l'option du bas "Rechercher une autre application", puis accédez au dossier BizHawk et sélectionnez EmuHawk.exe.
|
||||
|
||||
Un guide de configuration BizHawk alternatif ainsi que divers conseils de dépannage peuvent être trouvés
|
||||
Un guide de configuration BizHawk alternatif ainsi que divers conseils de dépannage sont disponibles
|
||||
[ici](https://wiki.ootrandomizer.com/index.php?title=Bizhawk).
|
||||
|
||||
## Configuration de votre fichier YAML
|
||||
## Créer un fichier de configuration (.yaml)
|
||||
|
||||
### Qu'est-ce qu'un fichier YAML et pourquoi en ai-je besoin ?
|
||||
### Qu'est-ce qu'un fichier de configuration et pourquoi en ai-je besoin ?
|
||||
|
||||
Votre fichier YAML contient un ensemble d'options de configuration qui fournissent au générateur des informations sur la façon dont il doit
|
||||
générer votre jeu. Chaque joueur d'un multimonde fournira son propre fichier YAML. Cette configuration permet à chaque joueur de profiter
|
||||
d'une expérience personnalisée à leur goût, et différents joueurs dans le même multimonde peuvent tous avoir des options différentes.
|
||||
Consultez le guide sur la configuration d'un YAML de base lors de la configuration de l'archipel.
|
||||
guide : [Guide de configuration de base de Multiworld](/tutorial/Archipelago/setup/en)
|
||||
|
||||
### Où puis-je obtenir un fichier YAML ?
|
||||
### Où puis-je obtenir un fichier de configuration (.yaml) ?
|
||||
|
||||
Un yaml OoT de base ressemblera à ceci. Il y a beaucoup d'options cosmétiques qui ont été supprimées pour le plaisir de ce
|
||||
tutoriel, si vous voulez voir une liste complète, téléchargez Archipelago depuis
|
||||
la [Archipelago Releases Page](https://github.com/ArchipelagoMW/Archipelago/releases) et recherchez l'exemple de fichier dans
|
||||
le dossier "Lecteurs".
|
||||
La page Paramètres du lecteur sur le site Web vous permet de configurer vos paramètres personnels et d'exporter un fichier de configuration depuis eux. Page des paramètres du joueur : [Page des paramètres du joueur d'Ocarina of Time](/games/Ocarina%20of%20Time/player-settings)
|
||||
|
||||
``` yaml
|
||||
description: Modèle par défaut d'Ocarina of Time # Utilisé pour décrire votre yaml. Utile si vous avez plusieurs fichiers
|
||||
# Votre nom dans le jeu. Les espaces seront remplacés par des underscores et il y a une limite de 16 caractères
|
||||
name: VotreNom
|
||||
game:
|
||||
Ocarina of Time: 1
|
||||
requires:
|
||||
version: 0.1.7 # Version d'Archipelago requise pour que ce yaml fonctionne comme prévu.
|
||||
# Options partagées prises en charge par tous les jeux :
|
||||
accessibility:
|
||||
items: 0 # Garantit que vous pourrez acquérir tous les articles, mais vous ne pourrez peut-être pas accéder à tous les emplacements
|
||||
locations: 50 # Garantit que vous pourrez accéder à tous les emplacements, et donc à tous les articles
|
||||
none: 0 # Garantit seulement que le jeu est battable. Vous ne pourrez peut-être pas accéder à tous les emplacements ou acquérir tous les objets
|
||||
progression_balancing: # Un système pour réduire le BK, comme dans les périodes où vous ne pouvez rien faire, en déplaçant vos éléments dans une sphère d'accès antérieure
|
||||
0: 0 # Choisissez un nombre inférieur si cela ne vous dérange pas d'avoir un multimonde plus long, ou si vous pouvez glitch / faire du hors logique.
|
||||
25: 0
|
||||
50: 50 # Faites en sorte que vous ayez probablement des choses à faire.
|
||||
99: 0 # Obtenez les éléments importants tôt et restez en tête de la progression.
|
||||
Ocarina of Time:
|
||||
logic_rules: # définit la logique utilisée pour le générateur.
|
||||
glitchless: 50
|
||||
glitched: 0
|
||||
no_logic: 0
|
||||
logic_no_night_tokens_without_suns_song: # Les skulltulas nocturnes nécessiteront logiquement le Chant du soleil.
|
||||
false: 50
|
||||
true: 0
|
||||
open_forest: # Définissez l'état de la forêt de Kokiri et du chemin vers l'arbre Mojo.
|
||||
open: 50
|
||||
closed_deku: 0
|
||||
closed: 0
|
||||
open_kakariko: # Définit l'état de la porte du village de Kakariko.
|
||||
open: 50
|
||||
zelda: 0
|
||||
closed: 0
|
||||
open_door_of_time: # Ouvre la Porte du Temps par défaut, sans le Chant du Temps.
|
||||
false: 0
|
||||
true: 50
|
||||
zora_fountain: # Définit l'état du roi Zora, bloquant le chemin vers la fontaine de Zora.
|
||||
open: 0
|
||||
adult: 0
|
||||
closed: 50
|
||||
gerudo_fortress: # Définit les conditions d'accès à la forteresse Gerudo.
|
||||
normal: 0
|
||||
fast: 50
|
||||
open: 0
|
||||
bridge: # Définit les exigences pour le pont arc-en-ciel.
|
||||
open: 0
|
||||
vanilla: 0
|
||||
stones: 0
|
||||
medallions: 50
|
||||
dungeons: 0
|
||||
tokens: 0
|
||||
trials: # Définit le nombre d'épreuves requises dans le Château de Ganon.
|
||||
# vous pouvez ajouter des valeurs supplémentaires entre minimum et maximum
|
||||
0: 50 # valeur minimale
|
||||
6: 0 # valeur maximale
|
||||
random: 0
|
||||
random-low: 0
|
||||
random-higt: 0
|
||||
starting_age: # Choisissez l'âge auquel Link commencera.
|
||||
child: 50
|
||||
adult: 0
|
||||
triforce_hunt: # Rassemblez des morceaux de la Triforce dispersés dans le monde entier pour terminer le jeu.
|
||||
false: 50
|
||||
true: 0
|
||||
triforce_goal: # Nombre de pièces Triforce nécessaires pour terminer le jeu. Nombre total placé déterminé par le paramètre Item Pool.
|
||||
# vous pouvez ajouter des valeurs supplémentaires entre minimum et maximum
|
||||
1: 0 # valeur minimale
|
||||
50: 0 # valeur maximale
|
||||
random: 0
|
||||
random-low: 0
|
||||
random-higt: 0
|
||||
20: 50
|
||||
bombchus_in_logic: # Les Bombchus sont correctement pris en compte dans la logique. Le premier pack trouvé aura 20 chus ; Kokiri Shop et Bazaar vendent des recharges ; bombchus ouvre Bombchu Bowling.
|
||||
false: 50
|
||||
true: 0
|
||||
bridge_stones: # Définissez le nombre de pierres spirituelles requises pour le pont arc-en-ciel.
|
||||
# vous pouvez ajouter des valeurs supplémentaires entre minimum et maximum
|
||||
0: 0 # valeur minimale
|
||||
3: 50 # valeur maximale
|
||||
random: 0
|
||||
random-low: 0
|
||||
random-high: 0
|
||||
bridge_medallions: # Définissez le nombre de médaillons requis pour le pont arc-en-ciel.
|
||||
# vous pouvez ajouter des valeurs supplémentaires entre minimum et maximum
|
||||
0: 0 # valeur minimale
|
||||
6: 50 # valeur maximale
|
||||
random: 0
|
||||
random-low: 0
|
||||
random-high: 0
|
||||
bridge_rewards: # Définissez le nombre de récompenses de donjon requises pour le pont arc-en-ciel.
|
||||
# vous pouvez ajouter des valeurs supplémentaires entre minimum et maximum
|
||||
0: 0 # valeur minimale
|
||||
9: 50 # valeur maximale
|
||||
random: 0
|
||||
random-low: 0
|
||||
random-high: 0
|
||||
bridge_tokens: # Définissez le nombre de jetons Gold Skulltula requis pour le pont arc-en-ciel.
|
||||
# vous pouvez ajouter des valeurs supplémentaires entre minimum et maximum
|
||||
0: 0 # valeur minimale
|
||||
100: 50 # valeur maximale
|
||||
random: 0
|
||||
random-low: 0
|
||||
random-high: 0
|
||||
shuffle_mapcompass: # Contrôle où mélanger les cartes et boussoles des donjons.
|
||||
remove: 0
|
||||
startwith: 50
|
||||
vanilla: 0
|
||||
dungeon: 0
|
||||
overworld: 0
|
||||
any_dungeon: 0
|
||||
keysanity: 0
|
||||
shuffle_smallkeys: # Contrôle où mélanger les petites clés de donjon.
|
||||
remove: 0
|
||||
vanilla: 0
|
||||
dungeon: 50
|
||||
overworld: 0
|
||||
any_dungeon: 0
|
||||
keysanity: 0
|
||||
shuffle_hideoutkeys: # Contrôle où mélanger les petites clés de la Forteresse Gerudo.
|
||||
vanilla: 50
|
||||
overworld: 0
|
||||
any_dungeon: 0
|
||||
keysanity: 0
|
||||
shuffle_bosskeys: # Contrôle où mélanger les clés du boss, à l'exception de la clé du boss du château de Ganon.
|
||||
remove: 0
|
||||
vanilla: 0
|
||||
dungeon: 50
|
||||
overworld: 0
|
||||
any_dungeon: 0
|
||||
keysanity: 0
|
||||
shuffle_ganon_bosskey: # Contrôle où mélanger la clé du patron du château de Ganon.
|
||||
remove: 50
|
||||
vanilla: 0
|
||||
dungeon: 0
|
||||
overworld: 0
|
||||
any_dungeon: 0
|
||||
keysanity: 0
|
||||
on_lacs: 0
|
||||
enhance_map_compass: # La carte indique si un donjon est vanille ou MQ. La boussole indique quelle est la récompense du donjon.
|
||||
false: 50
|
||||
true: 0
|
||||
lacs_condition: # Définissez les exigences pour la cinématique de la Flèche lumineuse dans le Temple du temps.
|
||||
vanilla: 50
|
||||
stones: 0
|
||||
medallions: 0
|
||||
dungeons: 0
|
||||
tokens: 0
|
||||
lacs_stones: # Définissez le nombre de pierres spirituelles requises pour le LACS.
|
||||
# vous pouvez ajouter des valeurs supplémentaires entre minimum et maximum
|
||||
0: 0 # valeur minimale
|
||||
3: 50 # valeur maximale
|
||||
random: 0
|
||||
random-low: 0
|
||||
random-high: 0
|
||||
lacs_medallions: # Définissez le nombre de médaillons requis pour LACS.
|
||||
# vous pouvez ajouter des valeurs supplémentaires entre minimum et maximum
|
||||
0: 0 # valeur minimale
|
||||
6: 50 # valeur maximale
|
||||
random: 0
|
||||
random-low: 0
|
||||
random-high: 0
|
||||
lacs_rewards: # Définissez le nombre de récompenses de donjon requises pour LACS.
|
||||
# vous pouvez ajouter des valeurs supplémentaires entre minimum et maximum
|
||||
0: 0 # valeur minimale
|
||||
9: 50 # valeur maximale
|
||||
random: 0
|
||||
random-low: 0
|
||||
random-high: 0
|
||||
lacs_tokens: # Définissez le nombre de jetons Gold Skulltula requis pour le LACS.
|
||||
# vous pouvez ajouter des valeurs supplémentaires entre minimum et maximum
|
||||
0: 0 # valeur minimale
|
||||
100: 50 # valeur maximale
|
||||
random: 0
|
||||
random-low: 0
|
||||
random-high: 0
|
||||
shuffle_song_items: # Définit où les chansons peuvent apparaître.
|
||||
song: 50
|
||||
dungeon: 0
|
||||
any: 0
|
||||
shopsanity: # Randomise le contenu de la boutique. Réglez sur "off" pour ne pas mélanger les magasins ; "0" mélange les magasins mais ne n'autorise pas les articles multimonde dans les magasins.
|
||||
0: 0
|
||||
1: 0
|
||||
2: 0
|
||||
3: 0
|
||||
4: 0
|
||||
random_value: 0
|
||||
off: 50
|
||||
tokensanity : # les récompenses en jetons des Skulltulas dorées sont mélangées dans la réserve.
|
||||
off: 50
|
||||
dungeons: 0
|
||||
overworld: 0
|
||||
all: 0
|
||||
shuffle_scrubs: # Mélangez les articles vendus par Business Scrubs et fixez les prix.
|
||||
off: 50
|
||||
low: 0
|
||||
regular: 0
|
||||
random_prices: 0
|
||||
shuffle_cows: # les vaches donnent des objets lorsque la chanson d'Epona est jouée.
|
||||
false: 50
|
||||
true: 0
|
||||
shuffle_kokiri_sword: # Mélangez l'épée Kokiri dans la réserve d'objets.
|
||||
false: 50
|
||||
true: 0
|
||||
shuffle_ocarinas: # Mélangez l'Ocarina des fées et l'Ocarina du temps dans la réserve d'objets.
|
||||
false: 50
|
||||
true: 0
|
||||
shuffle_weird_egg: # Mélangez l'œuf bizarre de Malon au château d'Hyrule.
|
||||
false: 50
|
||||
true: 0
|
||||
shuffle_gerudo_card: # Mélangez la carte de membre Gerudo dans la réserve d'objets.
|
||||
false: 50
|
||||
true: 0
|
||||
shuffle_beans: # Ajoute un paquet de 10 haricots au pool d'objets et change le vendeur de haricots pour qu'il vende un objet pour 60 roupies.
|
||||
false: 50
|
||||
true: 0
|
||||
shuffle_medigoron_carpet_salesman: # Mélangez les objets vendus par Medigoron et le vendeur de tapis Haunted Wasteland.
|
||||
false: 50
|
||||
true: 0
|
||||
skip_child_zelda: # le jeu commence avec la lettre de Zelda, l'objet de la berceuse de Zelda et les événements pertinents déjà terminés.
|
||||
false: 50
|
||||
true: 0
|
||||
no_escape_sequence: # Ignore la séquence d'effondrement de la tour entre les combats de Ganondorf et de Ganon.
|
||||
false: 50
|
||||
true: 0
|
||||
no_guard_stealth: # Le vide sanitaire du château d'Hyrule passe directement à Zelda.
|
||||
false: 50
|
||||
true: 0
|
||||
no_epona_race: # Epona peut toujours être invoquée avec Epona's Song.
|
||||
false: 50
|
||||
true: 0
|
||||
skip_some_minigame_phases: # Dampe Race et Horseback Archery donnent les deux récompenses si la deuxième condition est remplie lors de la première tentative.
|
||||
false: 50
|
||||
true: 0
|
||||
complete_mask_quest: # Tous les masques sont immédiatement disponibles à l'emprunt dans la boutique Happy Mask.
|
||||
false: 50
|
||||
true: 0
|
||||
useful_cutscenes: # Réactive la cinématique Poe dans le Temple de la forêt, Darunia dans le Temple du feu et l'introduction de Twinrova. Surtout utile pour les pépins.
|
||||
false: 50
|
||||
true: 0
|
||||
fast_chests: # Toutes les animations des coffres sont rapides. Si désactivé, les éléments principaux ont une animation lente.
|
||||
false: 50
|
||||
true: 0
|
||||
free_scarecrow: # Sortir l'ocarina près d'un point d'épouvantail fait apparaître Pierre sans avoir besoin de la chanson.
|
||||
false: 50
|
||||
true: 0
|
||||
fast_bunny_hood: # Bunny Hood vous permet de vous déplacer 1,5 fois plus vite comme dans Majora's Mask.
|
||||
false: 50
|
||||
true: 0
|
||||
chicken_count: # Contrôle le nombre de Cuccos pour qu'Anju donne un objet en tant qu'enfant.
|
||||
\# vous pouvez ajouter des valeurs supplémentaires entre le minimum et le maximum
|
||||
0: 0 # valeur minimale
|
||||
7: 50 # valeur maximale
|
||||
random: 0
|
||||
random-low: 0
|
||||
random-high: 0
|
||||
hints: # les pierres à potins peuvent donner des indices sur l'emplacement des objets.
|
||||
none: 0
|
||||
mask: 0
|
||||
agony: 0
|
||||
always: 50
|
||||
hint_dist: # Choisissez la distribution d'astuces à utiliser. Affecte la fréquence des indices forts, quels éléments sont toujours indiqués, etc.
|
||||
balanced: 50
|
||||
ddr: 0
|
||||
league: 0
|
||||
mw2: 0
|
||||
scrubs: 0
|
||||
strong: 0
|
||||
tournament: 0
|
||||
useless: 0
|
||||
very_strong: 0
|
||||
text_shuffle: # Randomise le texte dans le jeu pour un effet comique.
|
||||
none: 50
|
||||
except_hints: 0
|
||||
complete: 0
|
||||
damage_multiplier: # contrôle la quantité de dégâts subis par Link.
|
||||
half: 0
|
||||
normal: 50
|
||||
double: 0
|
||||
quadruple: 0
|
||||
ohko: 0
|
||||
no_collectible_hearts: # les cœurs ne tomberont pas des ennemis ou des objets.
|
||||
false: 50
|
||||
true: 0
|
||||
starting_tod: # Changer l'heure de début de la journée.
|
||||
default: 50
|
||||
sunrise: 0
|
||||
morning: 0
|
||||
noon: 0
|
||||
afternoon: 0
|
||||
sunset: 0
|
||||
evening: 0
|
||||
midnight: 0
|
||||
witching_hour: 0
|
||||
start_with_consumables: # Démarrez le jeu avec des Deku Sticks et des Deku Nuts pleins.
|
||||
false: 50
|
||||
true: 0
|
||||
start_with_rupees: # Commencez avec un portefeuille plein. Les mises à niveau de portefeuille rempliront également votre portefeuille.
|
||||
false: 50
|
||||
true: 0
|
||||
item_pool_value: # modifie le nombre d'objets disponibles dans le jeu.
|
||||
plentiful: 0
|
||||
balanced: 50
|
||||
scarce: 0
|
||||
minimal: 0
|
||||
junk_ice_traps: # Ajoute des pièges à glace au pool d'objets.
|
||||
off: 0
|
||||
normal: 50
|
||||
on: 0
|
||||
mayhem: 0
|
||||
onslaught: 0
|
||||
ice_trap_appearance: # modifie l'apparence des pièges à glace en tant qu'éléments autonomes.
|
||||
major_only: 50
|
||||
junk_only: 0
|
||||
anything: 0
|
||||
logic_earliest_adult_trade: # premier élément pouvant apparaître dans la séquence d'échange pour adultes.
|
||||
pocket_egg: 0
|
||||
pocket_cucco: 0
|
||||
cojiro: 0
|
||||
odd_mushroom: 0
|
||||
poachers_saw: 0
|
||||
broken_sword: 0
|
||||
prescription: 50
|
||||
eyeball_frog: 0
|
||||
eyedrops: 0
|
||||
claim_check: 0
|
||||
logic_latest_adult_trade: # Dernier élément pouvant apparaître dans la séquence d'échange pour adultes.
|
||||
pocket_egg: 0
|
||||
pocket_cucco: 0
|
||||
cojiro: 0
|
||||
odd_mushroom: 0
|
||||
poachers_saw: 0
|
||||
broken_sword: 0
|
||||
prescription: 0
|
||||
eyeball_frog: 0
|
||||
eyedrops: 0
|
||||
claim_check: 50
|
||||
### Vérification de votre fichier de configuration
|
||||
|
||||
```
|
||||
Si vous souhaitez valider votre fichier de configuration pour vous assurer qu'il fonctionne, vous pouvez le faire sur la page YAML Validator.
|
||||
YAML page du validateur : [page de validation YAML](/mysterycheck)
|
||||
|
||||
## Rejoindre une partie MultiWorld
|
||||
## Rejoindre un jeu multimonde
|
||||
|
||||
### Obtenez votre fichier de correctif OOT
|
||||
### Obtenez votre fichier OOT modifié
|
||||
|
||||
Lorsque vous rejoignez un jeu multimonde, il vous sera demandé de fournir votre fichier YAML à l'hébergeur. Une fois que c'est Fini,
|
||||
l'hébergeur vous fournira soit un lien pour télécharger votre fichier de données, soit un fichier zip contenant les données de chacun
|
||||
des dossiers. Votre fichier de données doit avoir une extension `.apz5`.
|
||||
Lorsque vous rejoignez un jeu multimonde, il vous sera demandé de fournir votre fichier YAML à celui qui l'héberge. Une fois cela fait, l'hébergeur vous fournira soit un lien pour télécharger votre fichier de données, soit un fichier zip contenant les données de chacun des dossiers. Votre fichier de données doit avoir une extension « .apz5 ».
|
||||
|
||||
Double-cliquez sur votre fichier `.apz5` pour démarrer votre client et démarrer le processus de patch ROM. Une fois le processus terminé
|
||||
(cela peut prendre un certain temps), le client et l'émulateur seront lancés automatiquement (si vous avez associé l'extension
|
||||
à l'émulateur comme recommandé).
|
||||
Double-cliquez sur votre fichier « .apz5 » pour démarrer votre client et démarrer le processus de correctif ROM. Une fois le processus terminé (cela peut prendre un certain temps), le client et l'émulateur seront automatiquement démarrés (si vous avez associé l'extension à l'émulateur comme recommandé).
|
||||
|
||||
### Connectez-vous au multiserveur
|
||||
|
||||
Une fois le client et l'émulateur démarrés, vous devez les connecter. Dans l'émulateur, cliquez sur "Outils"
|
||||
menu et sélectionnez "Console Lua". Cliquez sur le bouton du dossier ou appuyez sur Ctrl+O pour ouvrir un script Lua.
|
||||
Une fois le client et l'émulateur démarrés, vous devez les connecter. Accédez à votre dossier d'installation Archipelago, puis vers `data/lua`, et faites glisser et déposez le script `connector_oot.lua` sur la fenêtre principale d'EmuHawk. (Vous pourrez plutôt ouvrir depuis la console Lua manuellement, cliquez sur `Script` 〉 `Open Script` et accédez à `connector_oot.lua` avec le sélecteur de fichiers.)
|
||||
|
||||
Accédez à votre dossier d'installation Archipelago et ouvrez `data/lua/connector_oot.lua`.
|
||||
Pour connecter le client au multiserveur, mettez simplement `<adresse>:<port>` dans le champ de texte en haut et appuyez sur Entrée (si le serveur utilise un mot de passe, tapez dans le champ de texte inférieur `/connect <adresse>:<port> [mot de passe]`)
|
||||
|
||||
Pour connecter le client au multiserveur, mettez simplement `<adresse>:<port>` dans le champ de texte en haut et appuyez sur Entrée (si le
|
||||
le serveur utilise un mot de passe, saisissez dans le champ de texte inférieur `/connect <adresse>:<port> [mot de passe]`)
|
||||
|
||||
Vous êtes maintenant prêt à commencer votre aventure à Hyrule.
|
||||
Vous êtes maintenant prêt à commencer votre aventure dans Hyrule.
|
||||
@@ -7,7 +7,7 @@ import logging
|
||||
import os
|
||||
from typing import Any, Set, List, Dict, Optional, Tuple, ClassVar
|
||||
|
||||
from BaseClasses import ItemClassification, MultiWorld, Tutorial
|
||||
from BaseClasses import ItemClassification, MultiWorld, Tutorial, LocationProgressType
|
||||
from Fill import FillError, fill_restrictive
|
||||
from Options import Toggle
|
||||
import settings
|
||||
@@ -20,7 +20,7 @@ from .items import (ITEM_GROUPS, PokemonEmeraldItem, create_item_label_to_code_m
|
||||
offset_item_value)
|
||||
from .locations import (LOCATION_GROUPS, PokemonEmeraldLocation, create_location_label_to_id_map,
|
||||
create_locations_with_tags)
|
||||
from .options import (ItemPoolType, RandomizeWildPokemon, RandomizeBadges, RandomizeTrainerParties, RandomizeHms,
|
||||
from .options import (Goal, ItemPoolType, RandomizeWildPokemon, RandomizeBadges, RandomizeTrainerParties, RandomizeHms,
|
||||
RandomizeStarters, LevelUpMoves, RandomizeAbilities, RandomizeTypes, TmCompatibility,
|
||||
HmCompatibility, RandomizeStaticEncounters, NormanRequirement, PokemonEmeraldOptions)
|
||||
from .pokemon import get_random_species, get_random_move, get_random_damaging_move, get_random_type
|
||||
@@ -146,6 +146,60 @@ class PokemonEmeraldWorld(World):
|
||||
|
||||
self.multiworld.regions.extend(regions.values())
|
||||
|
||||
# Exclude locations which are always locked behind the player's goal
|
||||
def exclude_locations(location_names: List[str]):
|
||||
for location_name in location_names:
|
||||
try:
|
||||
self.multiworld.get_location(location_name,
|
||||
self.player).progress_type = LocationProgressType.EXCLUDED
|
||||
except KeyError:
|
||||
continue # Location not in multiworld
|
||||
|
||||
if self.options.goal == Goal.option_champion:
|
||||
# Always required to beat champion before receiving this
|
||||
exclude_locations([
|
||||
"Littleroot Town - S.S. Ticket from Norman"
|
||||
])
|
||||
|
||||
# S.S. Ticket requires beating champion, so ferry is not accessible until after goal
|
||||
if not self.options.enable_ferry:
|
||||
exclude_locations([
|
||||
"SS Tidal - Hidden Item in Lower Deck Trash Can",
|
||||
"SS Tidal - TM49 from Thief"
|
||||
])
|
||||
|
||||
# Construction workers don't move until champion is defeated
|
||||
if "Safari Zone Construction Workers" not in self.options.remove_roadblocks.value:
|
||||
exclude_locations([
|
||||
"Safari Zone NE - Hidden Item North",
|
||||
"Safari Zone NE - Hidden Item East",
|
||||
"Safari Zone NE - Item on Ledge",
|
||||
"Safari Zone SE - Hidden Item in South Grass 1",
|
||||
"Safari Zone SE - Hidden Item in South Grass 2",
|
||||
"Safari Zone SE - Item in Grass"
|
||||
])
|
||||
elif self.options.goal == Goal.option_norman:
|
||||
# If the player sets their options such that Surf or the Balance
|
||||
# Badge is vanilla, a very large number of locations become
|
||||
# "post-Norman". Similarly, access to the E4 may require you to
|
||||
# defeat Norman as an event or to get his badge, making postgame
|
||||
# locations inaccessible. Detecting these situations isn't trivial
|
||||
# and excluding all locations requiring Surf would be a bad idea.
|
||||
# So for now we just won't touch it and blame the user for
|
||||
# constructing their options in this way. Players usually expect
|
||||
# to only partially complete their world when playing this goal
|
||||
# anyway.
|
||||
|
||||
# Locations which are directly unlocked by defeating Norman.
|
||||
exclude_locations([
|
||||
"Petalburg Gym - Balance Badge",
|
||||
"Petalburg Gym - TM42 from Norman",
|
||||
"Petalburg City - HM03 from Wally's Uncle",
|
||||
"Dewford Town - TM36 from Sludge Bomb Man",
|
||||
"Mauville City - Basement Key from Wattson",
|
||||
"Mauville City - TM24 from Wattson"
|
||||
])
|
||||
|
||||
def create_items(self) -> None:
|
||||
item_locations: List[PokemonEmeraldLocation] = [
|
||||
location
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
# Pokémon Emerald
|
||||
|
||||
## Where is the settings page?
|
||||
## Where is the options page?
|
||||
|
||||
You can read through all the settings and generate a YAML [here](../player-settings).
|
||||
You can read through all the options and generate a YAML [here](../player-options).
|
||||
|
||||
## What does randomization do to this game?
|
||||
|
||||
This randomizer handles both item randomization and pokémon randomization. Badges, HMs, gifts from NPCs, and items on
|
||||
the ground can all be randomized. There are also many options for randomizing wild pokémon, starters, opponent pokémon,
|
||||
abilities, types, etc… You can even change a percentage of single battles into double battles. Check the
|
||||
[settings page](../player-settings) for a more comprehensive list of what can be changed.
|
||||
[options page](../player-options) for a more comprehensive list of what can be changed.
|
||||
|
||||
## What items and locations get randomized?
|
||||
|
||||
@@ -28,7 +28,7 @@ randomizer. Here are some of the more important ones:
|
||||
- You can have both bikes simultaneously
|
||||
- You can run or bike (almost) anywhere
|
||||
- The Wally catching tutorial is skipped
|
||||
- All text is instant, and with a setting it can be automatically progressed by holding A
|
||||
- All text is instant and, with an option, can be automatically progressed by holding A
|
||||
- When a Repel runs out, you will be prompted to use another
|
||||
- Many more minor improvements…
|
||||
|
||||
@@ -44,7 +44,7 @@ your inventory.
|
||||
## When the player receives an item, what happens?
|
||||
|
||||
You will only receive items while in the overworld and not during battles. Depending on your `Receive Item Messages`
|
||||
setting, the received item will either be silently added to your bag or you will be shown a text box with the item's
|
||||
option, the received item will either be silently added to your bag or you will be shown a text box with the item's
|
||||
name and the item will be added to your bag while a fanfare plays.
|
||||
|
||||
## Can I play offline?
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user