mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-03-28 07:23:27 -07:00
Merge branch 'ArchipelagoMW:main' into main
This commit is contained in:
@@ -325,15 +325,15 @@ class MultiWorld():
|
||||
def player_ids(self) -> Tuple[int, ...]:
|
||||
return tuple(range(1, self.players + 1))
|
||||
|
||||
@functools.lru_cache()
|
||||
@Utils.cache_self1
|
||||
def get_game_players(self, game_name: str) -> Tuple[int, ...]:
|
||||
return tuple(player for player in self.player_ids if self.game[player] == game_name)
|
||||
|
||||
@functools.lru_cache()
|
||||
@Utils.cache_self1
|
||||
def get_game_groups(self, game_name: str) -> Tuple[int, ...]:
|
||||
return tuple(group_id for group_id in self.groups if self.game[group_id] == game_name)
|
||||
|
||||
@functools.lru_cache()
|
||||
@Utils.cache_self1
|
||||
def get_game_worlds(self, game_name: str):
|
||||
return tuple(world for player, world in self.worlds.items() if
|
||||
player not in self.groups and self.game[player] == game_name)
|
||||
@@ -605,7 +605,7 @@ PathValue = Tuple[str, Optional["PathValue"]]
|
||||
|
||||
|
||||
class CollectionState():
|
||||
prog_items: typing.Counter[Tuple[str, int]]
|
||||
prog_items: Dict[int, Counter[str]]
|
||||
multiworld: MultiWorld
|
||||
reachable_regions: Dict[int, Set[Region]]
|
||||
blocked_connections: Dict[int, Set[Entrance]]
|
||||
@@ -617,7 +617,7 @@ class CollectionState():
|
||||
additional_copy_functions: List[Callable[[CollectionState, CollectionState], CollectionState]] = []
|
||||
|
||||
def __init__(self, parent: MultiWorld):
|
||||
self.prog_items = Counter()
|
||||
self.prog_items = {player: Counter() for player in parent.player_ids}
|
||||
self.multiworld = parent
|
||||
self.reachable_regions = {player: set() for player in parent.get_all_ids()}
|
||||
self.blocked_connections = {player: set() for player in parent.get_all_ids()}
|
||||
@@ -665,7 +665,7 @@ class CollectionState():
|
||||
|
||||
def copy(self) -> CollectionState:
|
||||
ret = CollectionState(self.multiworld)
|
||||
ret.prog_items = self.prog_items.copy()
|
||||
ret.prog_items = copy.deepcopy(self.prog_items)
|
||||
ret.reachable_regions = {player: copy.copy(self.reachable_regions[player]) for player in
|
||||
self.reachable_regions}
|
||||
ret.blocked_connections = {player: copy.copy(self.blocked_connections[player]) for player in
|
||||
@@ -709,23 +709,23 @@ class CollectionState():
|
||||
self.collect(event.item, True, event)
|
||||
|
||||
def has(self, item: str, player: int, count: int = 1) -> bool:
|
||||
return self.prog_items[item, player] >= count
|
||||
return self.prog_items[player][item] >= count
|
||||
|
||||
def has_all(self, items: Set[str], player: int) -> bool:
|
||||
"""Returns True if each item name of items is in state at least once."""
|
||||
return all(self.prog_items[item, player] for item in items)
|
||||
return all(self.prog_items[player][item] for item in items)
|
||||
|
||||
def has_any(self, items: Set[str], player: int) -> bool:
|
||||
"""Returns True if at least one item name of items is in state at least once."""
|
||||
return any(self.prog_items[item, player] for item in items)
|
||||
return any(self.prog_items[player][item] for item in items)
|
||||
|
||||
def count(self, item: str, player: int) -> int:
|
||||
return self.prog_items[item, player]
|
||||
return self.prog_items[player][item]
|
||||
|
||||
def has_group(self, item_name_group: str, player: int, count: int = 1) -> bool:
|
||||
found: int = 0
|
||||
for item_name in self.multiworld.worlds[player].item_name_groups[item_name_group]:
|
||||
found += self.prog_items[item_name, player]
|
||||
found += self.prog_items[player][item_name]
|
||||
if found >= count:
|
||||
return True
|
||||
return False
|
||||
@@ -733,11 +733,11 @@ class CollectionState():
|
||||
def count_group(self, item_name_group: str, player: int) -> int:
|
||||
found: int = 0
|
||||
for item_name in self.multiworld.worlds[player].item_name_groups[item_name_group]:
|
||||
found += self.prog_items[item_name, player]
|
||||
found += self.prog_items[player][item_name]
|
||||
return found
|
||||
|
||||
def item_count(self, item: str, player: int) -> int:
|
||||
return self.prog_items[item, player]
|
||||
return self.prog_items[player][item]
|
||||
|
||||
def collect(self, item: Item, event: bool = False, location: Optional[Location] = None) -> bool:
|
||||
if location:
|
||||
@@ -746,7 +746,7 @@ class CollectionState():
|
||||
changed = self.multiworld.worlds[item.player].collect(self, item)
|
||||
|
||||
if not changed and event:
|
||||
self.prog_items[item.name, item.player] += 1
|
||||
self.prog_items[item.player][item.name] += 1
|
||||
changed = True
|
||||
|
||||
self.stale[item.player] = True
|
||||
@@ -920,7 +920,7 @@ class Region:
|
||||
self.locations.append(location_type(self.player, location, address, self))
|
||||
|
||||
def connect(self, connecting_region: Region, name: Optional[str] = None,
|
||||
rule: Optional[Callable[[CollectionState], bool]] = None) -> None:
|
||||
rule: Optional[Callable[[CollectionState], bool]] = None) -> entrance_type:
|
||||
"""
|
||||
Connects this Region to another Region, placing the provided rule on the connection.
|
||||
|
||||
@@ -931,6 +931,7 @@ class Region:
|
||||
if rule:
|
||||
exit_.access_rule = rule
|
||||
exit_.connect(connecting_region)
|
||||
return exit_
|
||||
|
||||
def create_exit(self, name: str) -> Entrance:
|
||||
"""
|
||||
|
||||
17
Generate.py
17
Generate.py
@@ -7,8 +7,8 @@ import random
|
||||
import string
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
from collections import ChainMap, Counter
|
||||
from typing import Any, Callable, Dict, Tuple, Union
|
||||
from collections import Counter
|
||||
from typing import Any, Dict, Tuple, Union
|
||||
|
||||
import ModuleUpdate
|
||||
|
||||
@@ -225,7 +225,7 @@ def main(args=None, callback=ERmain):
|
||||
with open(os.path.join(args.outputpath if args.outputpath else ".", f"generate_{seed_name}.yaml"), "wt") as f:
|
||||
yaml.dump(important, f)
|
||||
|
||||
callback(erargs, seed)
|
||||
return callback(erargs, seed)
|
||||
|
||||
|
||||
def read_weights_yamls(path) -> Tuple[Any, ...]:
|
||||
@@ -639,6 +639,15 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
|
||||
if __name__ == '__main__':
|
||||
import atexit
|
||||
confirmation = atexit.register(input, "Press enter to close.")
|
||||
main()
|
||||
multiworld = main()
|
||||
if __debug__:
|
||||
import gc
|
||||
import sys
|
||||
import weakref
|
||||
weak = weakref.ref(multiworld)
|
||||
del multiworld
|
||||
gc.collect() # need to collect to deref all hard references
|
||||
assert not weak(), f"MultiWorld object was not de-allocated, it's referenced {sys.getrefcount(weak())} times." \
|
||||
" This would be a memory leak."
|
||||
# in case of error-free exit should not need confirmation
|
||||
atexit.unregister(confirmation)
|
||||
|
||||
@@ -207,12 +207,12 @@ class SNIContext(CommonContext):
|
||||
self.killing_player_task = asyncio.create_task(deathlink_kill_player(self))
|
||||
super(SNIContext, self).on_deathlink(data)
|
||||
|
||||
async def handle_deathlink_state(self, currently_dead: bool) -> None:
|
||||
async def handle_deathlink_state(self, currently_dead: bool, death_text: str = "") -> None:
|
||||
# in this state we only care about triggering a death send
|
||||
if self.death_state == DeathState.alive:
|
||||
if currently_dead:
|
||||
self.death_state = DeathState.dead
|
||||
await self.send_death()
|
||||
await self.send_death(death_text)
|
||||
# in this state we care about confirming a kill, to move state to dead
|
||||
elif self.death_state == DeathState.killing_player:
|
||||
# this is being handled in deathlink_kill_player(ctx) already
|
||||
|
||||
37
Utils.py
37
Utils.py
@@ -74,6 +74,8 @@ def snes_to_pc(value: int) -> int:
|
||||
|
||||
|
||||
RetType = typing.TypeVar("RetType")
|
||||
S = typing.TypeVar("S")
|
||||
T = typing.TypeVar("T")
|
||||
|
||||
|
||||
def cache_argsless(function: typing.Callable[[], RetType]) -> typing.Callable[[], RetType]:
|
||||
@@ -91,6 +93,31 @@ def cache_argsless(function: typing.Callable[[], RetType]) -> typing.Callable[[]
|
||||
return _wrap
|
||||
|
||||
|
||||
def cache_self1(function: typing.Callable[[S, T], RetType]) -> typing.Callable[[S, T], RetType]:
|
||||
"""Specialized cache for self + 1 arg. Does not keep global ref to self and skips building a dict key tuple."""
|
||||
|
||||
assert function.__code__.co_argcount == 2, "Can only cache 2 argument functions with this cache."
|
||||
|
||||
cache_name = f"__cache_{function.__name__}__"
|
||||
|
||||
@functools.wraps(function)
|
||||
def wrap(self: S, arg: T) -> RetType:
|
||||
cache: Optional[Dict[T, RetType]] = typing.cast(Optional[Dict[T, RetType]],
|
||||
getattr(self, cache_name, None))
|
||||
if cache is None:
|
||||
res = function(self, arg)
|
||||
setattr(self, cache_name, {arg: res})
|
||||
return res
|
||||
try:
|
||||
return cache[arg]
|
||||
except KeyError:
|
||||
res = function(self, arg)
|
||||
cache[arg] = res
|
||||
return res
|
||||
|
||||
return wrap
|
||||
|
||||
|
||||
def is_frozen() -> bool:
|
||||
return typing.cast(bool, getattr(sys, 'frozen', False))
|
||||
|
||||
@@ -147,12 +174,16 @@ def user_path(*path: str) -> str:
|
||||
if user_path.cached_path != local_path():
|
||||
import filecmp
|
||||
if not os.path.exists(user_path("manifest.json")) or \
|
||||
not os.path.exists(local_path("manifest.json")) or \
|
||||
not filecmp.cmp(local_path("manifest.json"), user_path("manifest.json"), shallow=True):
|
||||
import shutil
|
||||
for dn in ("Players", "data/sprites"):
|
||||
for dn in ("Players", "data/sprites", "data/lua"):
|
||||
shutil.copytree(local_path(dn), user_path(dn), dirs_exist_ok=True)
|
||||
for fn in ("manifest.json",):
|
||||
shutil.copy2(local_path(fn), user_path(fn))
|
||||
if not os.path.exists(local_path("manifest.json")):
|
||||
warnings.warn(f"Upgrading {user_path()} from something that is not a proper install")
|
||||
else:
|
||||
shutil.copy2(local_path("manifest.json"), user_path("manifest.json"))
|
||||
os.makedirs(user_path("worlds"), exist_ok=True)
|
||||
|
||||
return os.path.join(user_path.cached_path, *path)
|
||||
|
||||
|
||||
@@ -139,7 +139,13 @@ def create():
|
||||
weighted_options["games"][game_name] = {}
|
||||
weighted_options["games"][game_name]["gameSettings"] = game_options
|
||||
weighted_options["games"][game_name]["gameItems"] = tuple(world.item_names)
|
||||
weighted_options["games"][game_name]["gameItemGroups"] = [
|
||||
group for group in world.item_name_groups.keys() if group != "Everything"
|
||||
]
|
||||
weighted_options["games"][game_name]["gameLocations"] = tuple(world.location_names)
|
||||
weighted_options["games"][game_name]["gameLocationGroups"] = [
|
||||
group for group in world.location_name_groups.keys() if group != "Everywhere"
|
||||
]
|
||||
|
||||
with open(os.path.join(target_folder, 'weighted-options.json'), "w") as f:
|
||||
json.dump(weighted_options, f, indent=2, separators=(',', ': '))
|
||||
|
||||
@@ -43,7 +43,7 @@ const resetSettings = () => {
|
||||
};
|
||||
|
||||
const fetchSettingData = () => new Promise((resolve, reject) => {
|
||||
fetch(new Request(`${window.location.origin}/static/generated/weighted-settings.json`)).then((response) => {
|
||||
fetch(new Request(`${window.location.origin}/static/generated/weighted-options.json`)).then((response) => {
|
||||
try{ response.json().then((jsonObj) => resolve(jsonObj)); }
|
||||
catch(error){ reject(error); }
|
||||
});
|
||||
@@ -428,13 +428,13 @@ class GameSettings {
|
||||
const weightedSettingsDiv = this.#buildWeightedSettingsDiv();
|
||||
gameDiv.appendChild(weightedSettingsDiv);
|
||||
|
||||
const itemPoolDiv = this.#buildItemsDiv();
|
||||
const itemPoolDiv = this.#buildItemPoolDiv();
|
||||
gameDiv.appendChild(itemPoolDiv);
|
||||
|
||||
const hintsDiv = this.#buildHintsDiv();
|
||||
gameDiv.appendChild(hintsDiv);
|
||||
|
||||
const locationsDiv = this.#buildLocationsDiv();
|
||||
const locationsDiv = this.#buildPriorityExclusionDiv();
|
||||
gameDiv.appendChild(locationsDiv);
|
||||
|
||||
collapseButton.addEventListener('click', () => {
|
||||
@@ -734,107 +734,17 @@ class GameSettings {
|
||||
break;
|
||||
|
||||
case 'items-list':
|
||||
const itemsList = document.createElement('div');
|
||||
itemsList.classList.add('simple-list');
|
||||
|
||||
Object.values(this.data.gameItems).forEach((item) => {
|
||||
const itemRow = document.createElement('div');
|
||||
itemRow.classList.add('list-row');
|
||||
|
||||
const itemLabel = document.createElement('label');
|
||||
itemLabel.setAttribute('for', `${this.name}-${settingName}-${item}`)
|
||||
|
||||
const itemCheckbox = document.createElement('input');
|
||||
itemCheckbox.setAttribute('id', `${this.name}-${settingName}-${item}`);
|
||||
itemCheckbox.setAttribute('type', 'checkbox');
|
||||
itemCheckbox.setAttribute('data-game', this.name);
|
||||
itemCheckbox.setAttribute('data-setting', settingName);
|
||||
itemCheckbox.setAttribute('data-option', item.toString());
|
||||
itemCheckbox.addEventListener('change', (evt) => this.#updateListSetting(evt));
|
||||
if (this.current[settingName].includes(item)) {
|
||||
itemCheckbox.setAttribute('checked', '1');
|
||||
}
|
||||
|
||||
const itemName = document.createElement('span');
|
||||
itemName.innerText = item.toString();
|
||||
|
||||
itemLabel.appendChild(itemCheckbox);
|
||||
itemLabel.appendChild(itemName);
|
||||
|
||||
itemRow.appendChild(itemLabel);
|
||||
itemsList.appendChild((itemRow));
|
||||
});
|
||||
|
||||
const itemsList = this.#buildItemsDiv(settingName);
|
||||
settingWrapper.appendChild(itemsList);
|
||||
break;
|
||||
|
||||
case 'locations-list':
|
||||
const locationsList = document.createElement('div');
|
||||
locationsList.classList.add('simple-list');
|
||||
|
||||
Object.values(this.data.gameLocations).forEach((location) => {
|
||||
const locationRow = document.createElement('div');
|
||||
locationRow.classList.add('list-row');
|
||||
|
||||
const locationLabel = document.createElement('label');
|
||||
locationLabel.setAttribute('for', `${this.name}-${settingName}-${location}`)
|
||||
|
||||
const locationCheckbox = document.createElement('input');
|
||||
locationCheckbox.setAttribute('id', `${this.name}-${settingName}-${location}`);
|
||||
locationCheckbox.setAttribute('type', 'checkbox');
|
||||
locationCheckbox.setAttribute('data-game', this.name);
|
||||
locationCheckbox.setAttribute('data-setting', settingName);
|
||||
locationCheckbox.setAttribute('data-option', location.toString());
|
||||
locationCheckbox.addEventListener('change', (evt) => this.#updateListSetting(evt));
|
||||
if (this.current[settingName].includes(location)) {
|
||||
locationCheckbox.setAttribute('checked', '1');
|
||||
}
|
||||
|
||||
const locationName = document.createElement('span');
|
||||
locationName.innerText = location.toString();
|
||||
|
||||
locationLabel.appendChild(locationCheckbox);
|
||||
locationLabel.appendChild(locationName);
|
||||
|
||||
locationRow.appendChild(locationLabel);
|
||||
locationsList.appendChild((locationRow));
|
||||
});
|
||||
|
||||
const locationsList = this.#buildLocationsDiv(settingName);
|
||||
settingWrapper.appendChild(locationsList);
|
||||
break;
|
||||
|
||||
case 'custom-list':
|
||||
const customList = document.createElement('div');
|
||||
customList.classList.add('simple-list');
|
||||
|
||||
Object.values(this.data.gameSettings[settingName].options).forEach((listItem) => {
|
||||
const customListRow = document.createElement('div');
|
||||
customListRow.classList.add('list-row');
|
||||
|
||||
const customItemLabel = document.createElement('label');
|
||||
customItemLabel.setAttribute('for', `${this.name}-${settingName}-${listItem}`)
|
||||
|
||||
const customItemCheckbox = document.createElement('input');
|
||||
customItemCheckbox.setAttribute('id', `${this.name}-${settingName}-${listItem}`);
|
||||
customItemCheckbox.setAttribute('type', 'checkbox');
|
||||
customItemCheckbox.setAttribute('data-game', this.name);
|
||||
customItemCheckbox.setAttribute('data-setting', settingName);
|
||||
customItemCheckbox.setAttribute('data-option', listItem.toString());
|
||||
customItemCheckbox.addEventListener('change', (evt) => this.#updateListSetting(evt));
|
||||
if (this.current[settingName].includes(listItem)) {
|
||||
customItemCheckbox.setAttribute('checked', '1');
|
||||
}
|
||||
|
||||
const customItemName = document.createElement('span');
|
||||
customItemName.innerText = listItem.toString();
|
||||
|
||||
customItemLabel.appendChild(customItemCheckbox);
|
||||
customItemLabel.appendChild(customItemName);
|
||||
|
||||
customListRow.appendChild(customItemLabel);
|
||||
customList.appendChild((customListRow));
|
||||
});
|
||||
|
||||
const customList = this.#buildListDiv(settingName, this.data.gameSettings[settingName].options);
|
||||
settingWrapper.appendChild(customList);
|
||||
break;
|
||||
|
||||
@@ -849,7 +759,7 @@ class GameSettings {
|
||||
return settingsWrapper;
|
||||
}
|
||||
|
||||
#buildItemsDiv() {
|
||||
#buildItemPoolDiv() {
|
||||
const itemsDiv = document.createElement('div');
|
||||
itemsDiv.classList.add('items-div');
|
||||
|
||||
@@ -1058,35 +968,7 @@ class GameSettings {
|
||||
itemHintsWrapper.classList.add('hints-wrapper');
|
||||
itemHintsWrapper.innerText = 'Starting Item Hints';
|
||||
|
||||
const itemHintsDiv = document.createElement('div');
|
||||
itemHintsDiv.classList.add('simple-list');
|
||||
this.data.gameItems.forEach((item) => {
|
||||
const itemRow = document.createElement('div');
|
||||
itemRow.classList.add('list-row');
|
||||
|
||||
const itemLabel = document.createElement('label');
|
||||
itemLabel.setAttribute('for', `${this.name}-start_hints-${item}`);
|
||||
|
||||
const itemCheckbox = document.createElement('input');
|
||||
itemCheckbox.setAttribute('type', 'checkbox');
|
||||
itemCheckbox.setAttribute('id', `${this.name}-start_hints-${item}`);
|
||||
itemCheckbox.setAttribute('data-game', this.name);
|
||||
itemCheckbox.setAttribute('data-setting', 'start_hints');
|
||||
itemCheckbox.setAttribute('data-option', item);
|
||||
if (this.current.start_hints.includes(item)) {
|
||||
itemCheckbox.setAttribute('checked', 'true');
|
||||
}
|
||||
itemCheckbox.addEventListener('change', (evt) => this.#updateListSetting(evt));
|
||||
itemLabel.appendChild(itemCheckbox);
|
||||
|
||||
const itemName = document.createElement('span');
|
||||
itemName.innerText = item;
|
||||
itemLabel.appendChild(itemName);
|
||||
|
||||
itemRow.appendChild(itemLabel);
|
||||
itemHintsDiv.appendChild(itemRow);
|
||||
});
|
||||
|
||||
const itemHintsDiv = this.#buildItemsDiv('start_hints');
|
||||
itemHintsWrapper.appendChild(itemHintsDiv);
|
||||
itemHintsContainer.appendChild(itemHintsWrapper);
|
||||
|
||||
@@ -1095,35 +977,7 @@ class GameSettings {
|
||||
locationHintsWrapper.classList.add('hints-wrapper');
|
||||
locationHintsWrapper.innerText = 'Starting Location Hints';
|
||||
|
||||
const locationHintsDiv = document.createElement('div');
|
||||
locationHintsDiv.classList.add('simple-list');
|
||||
this.data.gameLocations.forEach((location) => {
|
||||
const locationRow = document.createElement('div');
|
||||
locationRow.classList.add('list-row');
|
||||
|
||||
const locationLabel = document.createElement('label');
|
||||
locationLabel.setAttribute('for', `${this.name}-start_location_hints-${location}`);
|
||||
|
||||
const locationCheckbox = document.createElement('input');
|
||||
locationCheckbox.setAttribute('type', 'checkbox');
|
||||
locationCheckbox.setAttribute('id', `${this.name}-start_location_hints-${location}`);
|
||||
locationCheckbox.setAttribute('data-game', this.name);
|
||||
locationCheckbox.setAttribute('data-setting', 'start_location_hints');
|
||||
locationCheckbox.setAttribute('data-option', location);
|
||||
if (this.current.start_location_hints.includes(location)) {
|
||||
locationCheckbox.setAttribute('checked', '1');
|
||||
}
|
||||
locationCheckbox.addEventListener('change', (evt) => this.#updateListSetting(evt));
|
||||
locationLabel.appendChild(locationCheckbox);
|
||||
|
||||
const locationName = document.createElement('span');
|
||||
locationName.innerText = location;
|
||||
locationLabel.appendChild(locationName);
|
||||
|
||||
locationRow.appendChild(locationLabel);
|
||||
locationHintsDiv.appendChild(locationRow);
|
||||
});
|
||||
|
||||
const locationHintsDiv = this.#buildLocationsDiv('start_location_hints');
|
||||
locationHintsWrapper.appendChild(locationHintsDiv);
|
||||
itemHintsContainer.appendChild(locationHintsWrapper);
|
||||
|
||||
@@ -1131,7 +985,7 @@ class GameSettings {
|
||||
return hintsDiv;
|
||||
}
|
||||
|
||||
#buildLocationsDiv() {
|
||||
#buildPriorityExclusionDiv() {
|
||||
const locationsDiv = document.createElement('div');
|
||||
locationsDiv.classList.add('locations-div');
|
||||
const locationsHeader = document.createElement('h3');
|
||||
@@ -1151,35 +1005,7 @@ class GameSettings {
|
||||
priorityLocationsWrapper.classList.add('locations-wrapper');
|
||||
priorityLocationsWrapper.innerText = 'Priority Locations';
|
||||
|
||||
const priorityLocationsDiv = document.createElement('div');
|
||||
priorityLocationsDiv.classList.add('simple-list');
|
||||
this.data.gameLocations.forEach((location) => {
|
||||
const locationRow = document.createElement('div');
|
||||
locationRow.classList.add('list-row');
|
||||
|
||||
const locationLabel = document.createElement('label');
|
||||
locationLabel.setAttribute('for', `${this.name}-priority_locations-${location}`);
|
||||
|
||||
const locationCheckbox = document.createElement('input');
|
||||
locationCheckbox.setAttribute('type', 'checkbox');
|
||||
locationCheckbox.setAttribute('id', `${this.name}-priority_locations-${location}`);
|
||||
locationCheckbox.setAttribute('data-game', this.name);
|
||||
locationCheckbox.setAttribute('data-setting', 'priority_locations');
|
||||
locationCheckbox.setAttribute('data-option', location);
|
||||
if (this.current.priority_locations.includes(location)) {
|
||||
locationCheckbox.setAttribute('checked', '1');
|
||||
}
|
||||
locationCheckbox.addEventListener('change', (evt) => this.#updateListSetting(evt));
|
||||
locationLabel.appendChild(locationCheckbox);
|
||||
|
||||
const locationName = document.createElement('span');
|
||||
locationName.innerText = location;
|
||||
locationLabel.appendChild(locationName);
|
||||
|
||||
locationRow.appendChild(locationLabel);
|
||||
priorityLocationsDiv.appendChild(locationRow);
|
||||
});
|
||||
|
||||
const priorityLocationsDiv = this.#buildLocationsDiv('priority_locations');
|
||||
priorityLocationsWrapper.appendChild(priorityLocationsDiv);
|
||||
locationsContainer.appendChild(priorityLocationsWrapper);
|
||||
|
||||
@@ -1188,35 +1014,7 @@ class GameSettings {
|
||||
excludeLocationsWrapper.classList.add('locations-wrapper');
|
||||
excludeLocationsWrapper.innerText = 'Exclude Locations';
|
||||
|
||||
const excludeLocationsDiv = document.createElement('div');
|
||||
excludeLocationsDiv.classList.add('simple-list');
|
||||
this.data.gameLocations.forEach((location) => {
|
||||
const locationRow = document.createElement('div');
|
||||
locationRow.classList.add('list-row');
|
||||
|
||||
const locationLabel = document.createElement('label');
|
||||
locationLabel.setAttribute('for', `${this.name}-exclude_locations-${location}`);
|
||||
|
||||
const locationCheckbox = document.createElement('input');
|
||||
locationCheckbox.setAttribute('type', 'checkbox');
|
||||
locationCheckbox.setAttribute('id', `${this.name}-exclude_locations-${location}`);
|
||||
locationCheckbox.setAttribute('data-game', this.name);
|
||||
locationCheckbox.setAttribute('data-setting', 'exclude_locations');
|
||||
locationCheckbox.setAttribute('data-option', location);
|
||||
if (this.current.exclude_locations.includes(location)) {
|
||||
locationCheckbox.setAttribute('checked', '1');
|
||||
}
|
||||
locationCheckbox.addEventListener('change', (evt) => this.#updateListSetting(evt));
|
||||
locationLabel.appendChild(locationCheckbox);
|
||||
|
||||
const locationName = document.createElement('span');
|
||||
locationName.innerText = location;
|
||||
locationLabel.appendChild(locationName);
|
||||
|
||||
locationRow.appendChild(locationLabel);
|
||||
excludeLocationsDiv.appendChild(locationRow);
|
||||
});
|
||||
|
||||
const excludeLocationsDiv = this.#buildLocationsDiv('exclude_locations');
|
||||
excludeLocationsWrapper.appendChild(excludeLocationsDiv);
|
||||
locationsContainer.appendChild(excludeLocationsWrapper);
|
||||
|
||||
@@ -1224,6 +1022,71 @@ class GameSettings {
|
||||
return locationsDiv;
|
||||
}
|
||||
|
||||
// Builds a div for a setting whose value is a list of locations.
|
||||
#buildLocationsDiv(setting) {
|
||||
return this.#buildListDiv(setting, this.data.gameLocations, this.data.gameLocationGroups);
|
||||
}
|
||||
|
||||
// Builds a div for a setting whose value is a list of items.
|
||||
#buildItemsDiv(setting) {
|
||||
return this.#buildListDiv(setting, this.data.gameItems, this.data.gameItemGroups);
|
||||
}
|
||||
|
||||
// Builds a div for a setting named `setting` with a list value that can
|
||||
// contain `items`.
|
||||
//
|
||||
// The `groups` option can be a list of additional options for this list
|
||||
// (usually `item_name_groups` or `location_name_groups`) that are displayed
|
||||
// in a special section at the top of the list.
|
||||
#buildListDiv(setting, items, groups = []) {
|
||||
const div = document.createElement('div');
|
||||
div.classList.add('simple-list');
|
||||
|
||||
groups.forEach((group) => {
|
||||
const row = this.#addListRow(setting, group);
|
||||
div.appendChild(row);
|
||||
});
|
||||
|
||||
if (groups.length > 0) {
|
||||
div.appendChild(document.createElement('hr'));
|
||||
}
|
||||
|
||||
items.forEach((item) => {
|
||||
const row = this.#addListRow(setting, item);
|
||||
div.appendChild(row);
|
||||
});
|
||||
|
||||
return div;
|
||||
}
|
||||
|
||||
// Builds and returns a row for a list of checkboxes.
|
||||
#addListRow(setting, item) {
|
||||
const row = document.createElement('div');
|
||||
row.classList.add('list-row');
|
||||
|
||||
const label = document.createElement('label');
|
||||
label.setAttribute('for', `${this.name}-${setting}-${item}`);
|
||||
|
||||
const checkbox = document.createElement('input');
|
||||
checkbox.setAttribute('type', 'checkbox');
|
||||
checkbox.setAttribute('id', `${this.name}-${setting}-${item}`);
|
||||
checkbox.setAttribute('data-game', this.name);
|
||||
checkbox.setAttribute('data-setting', setting);
|
||||
checkbox.setAttribute('data-option', item);
|
||||
if (this.current[setting].includes(item)) {
|
||||
checkbox.setAttribute('checked', '1');
|
||||
}
|
||||
checkbox.addEventListener('change', (evt) => this.#updateListSetting(evt));
|
||||
label.appendChild(checkbox);
|
||||
|
||||
const name = document.createElement('span');
|
||||
name.innerText = item;
|
||||
label.appendChild(name);
|
||||
|
||||
row.appendChild(label);
|
||||
return row;
|
||||
}
|
||||
|
||||
#updateRangeSetting(evt) {
|
||||
const setting = evt.target.getAttribute('data-setting');
|
||||
const option = evt.target.getAttribute('data-option');
|
||||
|
||||
@@ -292,6 +292,12 @@ html{
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
#weighted-settings .simple-list hr{
|
||||
width: calc(100% - 2px);
|
||||
margin: 2px auto;
|
||||
border-bottom: 1px solid rgb(255 255 255 / 0.6);
|
||||
}
|
||||
|
||||
#weighted-settings .invisible{
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -153,7 +153,7 @@
|
||||
{%- endif -%}
|
||||
{% endif %}
|
||||
{%- endfor -%}
|
||||
<td class="center-column">{{ percent_total_checks_done[team][player] }}</td>
|
||||
<td class="center-column">{{ "{0:.2f}".format(percent_total_checks_done[team][player]) }}</td>
|
||||
{%- if activity_timers[(team, player)] -%}
|
||||
<td class="center-column">{{ activity_timers[(team, player)].total_seconds() }}</td>
|
||||
{%- else -%}
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
<td class="center-column" data-sort="{{ checks["Total"] }}">
|
||||
{{ checks["Total"] }}/{{ locations[player] | length }}
|
||||
</td>
|
||||
<td class="center-column">{{ percent_total_checks_done[team][player] }}</td>
|
||||
<td class="center-column">{{ "{0:.2f}".format(percent_total_checks_done[team][player]) }}</td>
|
||||
{%- if activity_timers[team, player] -%}
|
||||
<td class="center-column">{{ activity_timers[team, player].total_seconds() }}</td>
|
||||
{%- else -%}
|
||||
@@ -72,7 +72,13 @@
|
||||
<td>All Games</td>
|
||||
<td>{{ completed_worlds }}/{{ players|length }} Complete</td>
|
||||
<td class="center-column">{{ players.values()|sum(attribute='Total') }}/{{ total_locations[team] }}</td>
|
||||
<td class="center-column">{{ (players.values()|sum(attribute='Total') / total_locations[team] * 100) | int }}</td>
|
||||
<td class="center-column">
|
||||
{% if total_locations[team] == 0 %}
|
||||
100
|
||||
{% else %}
|
||||
{{ "{0:.2f}".format(players.values()|sum(attribute='Total') / total_locations[team] * 100) }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="center-column last-activity"></td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
|
||||
@@ -1532,9 +1532,11 @@ def _get_multiworld_tracker_data(tracker: UUID) -> typing.Optional[typing.Dict[s
|
||||
continue
|
||||
player_locations = locations[player]
|
||||
checks_done[team][player]["Total"] = len(locations_checked)
|
||||
percent_total_checks_done[team][player] = int(checks_done[team][player]["Total"] /
|
||||
len(player_locations) * 100) \
|
||||
if player_locations else 100
|
||||
percent_total_checks_done[team][player] = (
|
||||
checks_done[team][player]["Total"] / len(player_locations) * 100
|
||||
if player_locations
|
||||
else 100
|
||||
)
|
||||
|
||||
activity_timers = {}
|
||||
now = datetime.datetime.utcnow()
|
||||
@@ -1690,10 +1692,13 @@ def get_LttP_multiworld_tracker(tracker: UUID):
|
||||
for recipient in recipients:
|
||||
attribute_item(team, recipient, item)
|
||||
checks_done[team][player][player_location_to_area[player][location]] += 1
|
||||
checks_done[team][player]["Total"] += 1
|
||||
percent_total_checks_done[team][player] = int(
|
||||
checks_done[team][player]["Total"] / len(player_locations) * 100) if \
|
||||
player_locations else 100
|
||||
checks_done[team][player]["Total"] = len(locations_checked)
|
||||
|
||||
percent_total_checks_done[team][player] = (
|
||||
checks_done[team][player]["Total"] / len(player_locations) * 100
|
||||
if player_locations
|
||||
else 100
|
||||
)
|
||||
|
||||
for (team, player), game_state in multisave.get("client_game_state", {}).items():
|
||||
if player in groups:
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import sys
|
||||
import typing
|
||||
import unittest
|
||||
from argparse import Namespace
|
||||
@@ -107,11 +108,36 @@ class WorldTestBase(unittest.TestCase):
|
||||
game: typing.ClassVar[str] # define game name in subclass, example "Secret of Evermore"
|
||||
auto_construct: typing.ClassVar[bool] = True
|
||||
""" automatically set up a world for each test in this class """
|
||||
memory_leak_tested: typing.ClassVar[bool] = False
|
||||
""" remember if memory leak test was already done for this class """
|
||||
|
||||
def setUp(self) -> None:
|
||||
if self.auto_construct:
|
||||
self.world_setup()
|
||||
|
||||
def tearDown(self) -> None:
|
||||
if self.__class__.memory_leak_tested or not self.options or not self.constructed or \
|
||||
sys.version_info < (3, 11, 0): # the leak check in tearDown fails in py<3.11 for an unknown reason
|
||||
# only run memory leak test once per class, only for constructed with non-default options
|
||||
# default options will be tested in test/general
|
||||
super().tearDown()
|
||||
return
|
||||
|
||||
import gc
|
||||
import weakref
|
||||
weak = weakref.ref(self.multiworld)
|
||||
for attr_name in dir(self): # delete all direct references to MultiWorld and World
|
||||
attr: object = typing.cast(object, getattr(self, attr_name))
|
||||
if type(attr) is MultiWorld or isinstance(attr, AutoWorld.World):
|
||||
delattr(self, attr_name)
|
||||
state_cache: typing.Optional[typing.Dict[typing.Any, typing.Any]] = getattr(self, "_state_cache", None)
|
||||
if state_cache is not None: # in case of multiple inheritance with TestBase, we need to clear its cache
|
||||
state_cache.clear()
|
||||
gc.collect()
|
||||
self.__class__.memory_leak_tested = True
|
||||
self.assertFalse(weak(), f"World {getattr(self, 'game', '')} leaked MultiWorld object")
|
||||
super().tearDown()
|
||||
|
||||
def world_setup(self, seed: typing.Optional[int] = None) -> None:
|
||||
if type(self) is WorldTestBase or \
|
||||
(hasattr(WorldTestBase, self._testMethodName)
|
||||
|
||||
@@ -455,8 +455,8 @@ class TestFillRestrictive(unittest.TestCase):
|
||||
location.place_locked_item(item)
|
||||
multi_world.state.sweep_for_events()
|
||||
multi_world.state.sweep_for_events()
|
||||
self.assertTrue(multi_world.state.prog_items[item.name, item.player], "Sweep did not collect - Test flawed")
|
||||
self.assertEqual(multi_world.state.prog_items[item.name, item.player], 1, "Sweep collected multiple times")
|
||||
self.assertTrue(multi_world.state.prog_items[item.player][item.name], "Sweep did not collect - Test flawed")
|
||||
self.assertEqual(multi_world.state.prog_items[item.player][item.name], 1, "Sweep collected multiple times")
|
||||
|
||||
def test_correct_item_instance_removed_from_pool(self):
|
||||
"""Test that a placed item gets removed from the submitted pool"""
|
||||
|
||||
16
test/general/test_memory.py
Normal file
16
test/general/test_memory.py
Normal file
@@ -0,0 +1,16 @@
|
||||
import unittest
|
||||
|
||||
from worlds.AutoWorld import AutoWorldRegister
|
||||
from . import setup_solo_multiworld
|
||||
|
||||
|
||||
class TestWorldMemory(unittest.TestCase):
|
||||
def test_leak(self):
|
||||
"""Tests that worlds don't leak references to MultiWorld or themselves with default options."""
|
||||
import gc
|
||||
import weakref
|
||||
for game_name, world_type in AutoWorldRegister.world_types.items():
|
||||
with self.subTest("Game", game_name=game_name):
|
||||
weak = weakref.ref(setup_solo_multiworld(world_type))
|
||||
gc.collect()
|
||||
self.assertFalse(weak(), "World leaked a reference")
|
||||
66
test/utils/test_caches.py
Normal file
66
test/utils/test_caches.py
Normal file
@@ -0,0 +1,66 @@
|
||||
# Tests for caches in Utils.py
|
||||
|
||||
import unittest
|
||||
from typing import Any
|
||||
|
||||
from Utils import cache_argsless, cache_self1
|
||||
|
||||
|
||||
class TestCacheArgless(unittest.TestCase):
|
||||
def test_cache(self) -> None:
|
||||
@cache_argsless
|
||||
def func_argless() -> object:
|
||||
return object()
|
||||
|
||||
self.assertTrue(func_argless() is func_argless())
|
||||
|
||||
if __debug__: # assert only available with __debug__
|
||||
def test_invalid_decorator(self) -> None:
|
||||
with self.assertRaises(Exception):
|
||||
@cache_argsless # type: ignore[arg-type]
|
||||
def func_with_arg(_: Any) -> None:
|
||||
pass
|
||||
|
||||
|
||||
class TestCacheSelf1(unittest.TestCase):
|
||||
def test_cache(self) -> None:
|
||||
class Cls:
|
||||
@cache_self1
|
||||
def func(self, _: Any) -> object:
|
||||
return object()
|
||||
|
||||
o1 = Cls()
|
||||
o2 = Cls()
|
||||
self.assertTrue(o1.func(1) is o1.func(1))
|
||||
self.assertFalse(o1.func(1) is o1.func(2))
|
||||
self.assertFalse(o1.func(1) is o2.func(1))
|
||||
|
||||
def test_gc(self) -> None:
|
||||
# verify that we don't keep a global reference
|
||||
import gc
|
||||
import weakref
|
||||
|
||||
class Cls:
|
||||
@cache_self1
|
||||
def func(self, _: Any) -> object:
|
||||
return object()
|
||||
|
||||
o = Cls()
|
||||
_ = o.func(o) # keep a hard ref to the result
|
||||
r = weakref.ref(o) # keep weak ref to the cache
|
||||
del o # remove hard ref to the cache
|
||||
gc.collect()
|
||||
self.assertFalse(r()) # weak ref should be dead now
|
||||
|
||||
if __debug__: # assert only available with __debug__
|
||||
def test_no_self(self) -> None:
|
||||
with self.assertRaises(Exception):
|
||||
@cache_self1 # type: ignore[arg-type]
|
||||
def func() -> Any:
|
||||
pass
|
||||
|
||||
def test_too_many_args(self) -> None:
|
||||
with self.assertRaises(Exception):
|
||||
@cache_self1 # type: ignore[arg-type]
|
||||
def func(_1: Any, _2: Any, _3: Any) -> Any:
|
||||
pass
|
||||
@@ -414,16 +414,16 @@ class World(metaclass=AutoWorldRegister):
|
||||
def collect(self, state: "CollectionState", item: "Item") -> bool:
|
||||
name = self.collect_item(state, item)
|
||||
if name:
|
||||
state.prog_items[name, self.player] += 1
|
||||
state.prog_items[self.player][name] += 1
|
||||
return True
|
||||
return False
|
||||
|
||||
def remove(self, state: "CollectionState", item: "Item") -> bool:
|
||||
name = self.collect_item(state, item, True)
|
||||
if name:
|
||||
state.prog_items[name, self.player] -= 1
|
||||
if state.prog_items[name, self.player] < 1:
|
||||
del (state.prog_items[name, self.player])
|
||||
state.prog_items[self.player][name] -= 1
|
||||
if state.prog_items[self.player][name] < 1:
|
||||
del (state.prog_items[self.player][name])
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
@@ -5,19 +5,20 @@ import typing
|
||||
import warnings
|
||||
import zipimport
|
||||
|
||||
folder = os.path.dirname(__file__)
|
||||
from Utils import user_path, local_path
|
||||
|
||||
__all__ = {
|
||||
local_folder = os.path.dirname(__file__)
|
||||
user_folder = user_path("worlds") if user_path() != local_path() else None
|
||||
|
||||
__all__ = (
|
||||
"lookup_any_item_id_to_name",
|
||||
"lookup_any_location_id_to_name",
|
||||
"network_data_package",
|
||||
"AutoWorldRegister",
|
||||
"world_sources",
|
||||
"folder",
|
||||
}
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from .AutoWorld import World
|
||||
"local_folder",
|
||||
"user_folder",
|
||||
)
|
||||
|
||||
|
||||
class GamesData(typing.TypedDict):
|
||||
@@ -41,13 +42,13 @@ class WorldSource(typing.NamedTuple):
|
||||
is_zip: bool = False
|
||||
relative: bool = True # relative to regular world import folder
|
||||
|
||||
def __repr__(self):
|
||||
def __repr__(self) -> str:
|
||||
return f"{self.__class__.__name__}({self.path}, is_zip={self.is_zip}, relative={self.relative})"
|
||||
|
||||
@property
|
||||
def resolved_path(self) -> str:
|
||||
if self.relative:
|
||||
return os.path.join(folder, self.path)
|
||||
return os.path.join(local_folder, self.path)
|
||||
return self.path
|
||||
|
||||
def load(self) -> bool:
|
||||
@@ -56,6 +57,7 @@ class WorldSource(typing.NamedTuple):
|
||||
importer = zipimport.zipimporter(self.resolved_path)
|
||||
if hasattr(importer, "find_spec"): # new in Python 3.10
|
||||
spec = importer.find_spec(os.path.basename(self.path).rsplit(".", 1)[0])
|
||||
assert spec, f"{self.path} is not a loadable module"
|
||||
mod = importlib.util.module_from_spec(spec)
|
||||
else: # TODO: remove with 3.8 support
|
||||
mod = importer.load_module(os.path.basename(self.path).rsplit(".", 1)[0])
|
||||
@@ -72,7 +74,7 @@ class WorldSource(typing.NamedTuple):
|
||||
importlib.import_module(f".{self.path}", "worlds")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
# A single world failing can still mean enough is working for the user, log and carry on
|
||||
import traceback
|
||||
import io
|
||||
@@ -87,14 +89,16 @@ class WorldSource(typing.NamedTuple):
|
||||
|
||||
# find potential world containers, currently folders and zip-importable .apworld's
|
||||
world_sources: typing.List[WorldSource] = []
|
||||
file: os.DirEntry # for me (Berserker) at least, PyCharm doesn't seem to infer the type correctly
|
||||
for file in os.scandir(folder):
|
||||
# prevent loading of __pycache__ and allow _* for non-world folders, disable files/folders starting with "."
|
||||
if not file.name.startswith(("_", ".")):
|
||||
if file.is_dir():
|
||||
world_sources.append(WorldSource(file.name))
|
||||
elif file.is_file() and file.name.endswith(".apworld"):
|
||||
world_sources.append(WorldSource(file.name, is_zip=True))
|
||||
for folder in (folder for folder in (user_folder, local_folder) if folder):
|
||||
relative = folder == local_folder
|
||||
for entry in os.scandir(folder):
|
||||
# prevent loading of __pycache__ and allow _* for non-world folders, disable files/folders starting with "."
|
||||
if not entry.name.startswith(("_", ".")):
|
||||
file_name = entry.name if relative else os.path.join(folder, entry.name)
|
||||
if entry.is_dir():
|
||||
world_sources.append(WorldSource(file_name, relative=relative))
|
||||
elif entry.is_file() and entry.name.endswith(".apworld"):
|
||||
world_sources.append(WorldSource(file_name, is_zip=True, relative=relative))
|
||||
|
||||
# import all submodules to trigger AutoWorldRegister
|
||||
world_sources.sort()
|
||||
@@ -105,7 +109,7 @@ lookup_any_item_id_to_name = {}
|
||||
lookup_any_location_id_to_name = {}
|
||||
games: typing.Dict[str, GamesPackage] = {}
|
||||
|
||||
from .AutoWorld import AutoWorldRegister
|
||||
from .AutoWorld import AutoWorldRegister # noqa: E402
|
||||
|
||||
# Build the data package for each game.
|
||||
for world_name, world in AutoWorldRegister.world_types.items():
|
||||
|
||||
@@ -520,7 +520,8 @@ class ALTTPSNIClient(SNIClient):
|
||||
gamemode = await snes_read(ctx, WRAM_START + 0x10, 1)
|
||||
if "DeathLink" in ctx.tags and gamemode and ctx.last_death_link + 1 < time.time():
|
||||
currently_dead = gamemode[0] in DEATH_MODES
|
||||
await ctx.handle_deathlink_state(currently_dead)
|
||||
await ctx.handle_deathlink_state(currently_dead,
|
||||
ctx.player_names[ctx.slot] + " ran out of hearts." if ctx.slot else "")
|
||||
|
||||
gameend = await snes_read(ctx, SAVEDATA_START + 0x443, 1)
|
||||
game_timer = await snes_read(ctx, SAVEDATA_START + 0x42E, 4)
|
||||
|
||||
@@ -31,7 +31,7 @@ def fake_pearl_state(state, player):
|
||||
if state.has('Moon Pearl', player):
|
||||
return state
|
||||
fake_state = state.copy()
|
||||
fake_state.prog_items['Moon Pearl', player] += 1
|
||||
fake_state.prog_items[player]['Moon Pearl'] += 1
|
||||
return fake_state
|
||||
|
||||
|
||||
|
||||
@@ -830,4 +830,4 @@ class ALttPLogic(LogicMixin):
|
||||
return True
|
||||
if self.multiworld.smallkey_shuffle[player] == smallkey_shuffle.option_universal:
|
||||
return can_buy_unlimited(self, 'Small Key (Universal)', player)
|
||||
return self.prog_items[item, player] >= count
|
||||
return self.prog_items[player][item] >= count
|
||||
|
||||
@@ -5,12 +5,7 @@ from ..generic.Rules import set_rule
|
||||
|
||||
class ArchipIDLELogic(LogicMixin):
|
||||
def _archipidle_location_is_accessible(self, player_id, items_required):
|
||||
items_received = 0
|
||||
for item in self.prog_items:
|
||||
if item[1] == player_id:
|
||||
items_received += 1
|
||||
|
||||
return items_received >= items_required
|
||||
return sum(self.prog_items[player_id].values()) >= items_required
|
||||
|
||||
|
||||
def set_rules(world: MultiWorld, player: int):
|
||||
|
||||
@@ -14,11 +14,18 @@ many checks as you have gained items, plus five to start with being available.
|
||||
## When the player receives an item, what happens?
|
||||
|
||||
When the player receives an item in ChecksFinder, it either can make the future boards they play be bigger in width or
|
||||
height, or add a new bomb to the future boards, with a limit to having up to one fifth of the _current_ board being
|
||||
bombs. The items you have gained _before_ the current board was made will be said at the bottom of the screen as a number
|
||||
height, or add a new bomb to the future boards, with a limit to having up to one fifth of the _current_ board being
|
||||
bombs. The items you have gained _before_ the current board was made will be said at the bottom of the screen as a
|
||||
number
|
||||
next to an icon, the number is how many you have gotten and the icon represents which item it is.
|
||||
|
||||
## What is the victory condition?
|
||||
|
||||
Victory is achieved when the player wins a board they were given after they have received all of their Map Width, Map
|
||||
Height, and Map Bomb items. The game will say at the bottom of the screen how many of each you have received.
|
||||
Height, and Map Bomb items. The game will say at the bottom of the screen how many of each you have received.
|
||||
|
||||
## Unique Local Commands
|
||||
|
||||
The following command is only available when using the ChecksFinderClient to play with Archipelago.
|
||||
|
||||
- `/resync` Manually trigger a resync.
|
||||
|
||||
@@ -12,11 +12,11 @@ def create_event(player, event: str) -> DLCQuestItem:
|
||||
|
||||
|
||||
def has_enough_coin(player: int, coin: int):
|
||||
return lambda state: state.prog_items[" coins", player] >= coin
|
||||
return lambda state: state.prog_items[player][" coins"] >= coin
|
||||
|
||||
|
||||
def has_enough_coin_freemium(player: int, coin: int):
|
||||
return lambda state: state.prog_items[" coins freemium", player] >= coin
|
||||
return lambda state: state.prog_items[player][" coins freemium"] >= coin
|
||||
|
||||
|
||||
def set_rules(world, player, World_Options: Options.DLCQuestOptions):
|
||||
|
||||
@@ -92,7 +92,7 @@ class DLCqworld(World):
|
||||
if change:
|
||||
suffix = item.coin_suffix
|
||||
if suffix:
|
||||
state.prog_items[suffix, self.player] += item.coins
|
||||
state.prog_items[self.player][suffix] += item.coins
|
||||
return change
|
||||
|
||||
def remove(self, state: CollectionState, item: DLCQuestItem) -> bool:
|
||||
@@ -100,5 +100,5 @@ class DLCqworld(World):
|
||||
if change:
|
||||
suffix = item.coin_suffix
|
||||
if suffix:
|
||||
state.prog_items[suffix, self.player] -= item.coins
|
||||
state.prog_items[self.player][suffix] -= item.coins
|
||||
return change
|
||||
|
||||
@@ -26,6 +26,7 @@ All local and remote items appear the same. Final Fantasy will say that you rece
|
||||
emulator will display what was found external to the in-game text box.
|
||||
|
||||
## Unique Local Commands
|
||||
The following command is only available when using the FF1Client for the Final Fantasy Randomizer.
|
||||
The following commands are only available when using the FF1Client for the Final Fantasy Randomizer.
|
||||
|
||||
- `/nes` Shows the current status of the NES connection.
|
||||
- `/toggle_msgs` Toggle displaying messages in EmuHawk
|
||||
|
||||
@@ -517,12 +517,12 @@ class HKWorld(World):
|
||||
change = super(HKWorld, self).collect(state, item)
|
||||
if change:
|
||||
for effect_name, effect_value in item_effects.get(item.name, {}).items():
|
||||
state.prog_items[effect_name, item.player] += effect_value
|
||||
state.prog_items[item.player][effect_name] += effect_value
|
||||
if item.name in {"Left_Mothwing_Cloak", "Right_Mothwing_Cloak"}:
|
||||
if state.prog_items.get(('RIGHTDASH', item.player), 0) and \
|
||||
state.prog_items.get(('LEFTDASH', item.player), 0):
|
||||
(state.prog_items["RIGHTDASH", item.player], state.prog_items["LEFTDASH", item.player]) = \
|
||||
([max(state.prog_items["RIGHTDASH", item.player], state.prog_items["LEFTDASH", item.player])] * 2)
|
||||
if state.prog_items[item.player].get('RIGHTDASH', 0) and \
|
||||
state.prog_items[item.player].get('LEFTDASH', 0):
|
||||
(state.prog_items[item.player]["RIGHTDASH"], state.prog_items[item.player]["LEFTDASH"]) = \
|
||||
([max(state.prog_items[item.player]["RIGHTDASH"], state.prog_items[item.player]["LEFTDASH"])] * 2)
|
||||
return change
|
||||
|
||||
def remove(self, state, item: HKItem) -> bool:
|
||||
@@ -530,9 +530,9 @@ class HKWorld(World):
|
||||
|
||||
if change:
|
||||
for effect_name, effect_value in item_effects.get(item.name, {}).items():
|
||||
if state.prog_items[effect_name, item.player] == effect_value:
|
||||
del state.prog_items[effect_name, item.player]
|
||||
state.prog_items[effect_name, item.player] -= effect_value
|
||||
if state.prog_items[item.player][effect_name] == effect_value:
|
||||
del state.prog_items[item.player][effect_name]
|
||||
state.prog_items[item.player][effect_name] -= effect_value
|
||||
|
||||
return change
|
||||
|
||||
|
||||
@@ -124,13 +124,13 @@ class GameStateAdapater:
|
||||
# Don't allow any money usage if you can't get back wasted rupees
|
||||
if item == "RUPEES":
|
||||
if can_farm_rupees(self.state, self.player):
|
||||
return self.state.prog_items["RUPEES", self.player]
|
||||
return self.state.prog_items[self.player]["RUPEES"]
|
||||
return 0
|
||||
elif item.endswith("_USED"):
|
||||
return 0
|
||||
else:
|
||||
item = ladxr_item_to_la_item_name[item]
|
||||
return self.state.prog_items.get((item, self.player), default)
|
||||
return self.state.prog_items[self.player].get(item, default)
|
||||
|
||||
|
||||
class LinksAwakeningEntrance(Entrance):
|
||||
|
||||
@@ -513,7 +513,7 @@ class LinksAwakeningWorld(World):
|
||||
change = super().collect(state, item)
|
||||
if change:
|
||||
rupees = self.rupees.get(item.name, 0)
|
||||
state.prog_items["RUPEES", item.player] += rupees
|
||||
state.prog_items[item.player]["RUPEES"] += rupees
|
||||
|
||||
return change
|
||||
|
||||
@@ -521,6 +521,6 @@ class LinksAwakeningWorld(World):
|
||||
change = super().remove(state, item)
|
||||
if change:
|
||||
rupees = self.rupees.get(item.name, 0)
|
||||
state.prog_items["RUPEES", item.player] -= rupees
|
||||
state.prog_items[item.player]["RUPEES"] -= rupees
|
||||
|
||||
return change
|
||||
|
||||
@@ -188,6 +188,6 @@ class MessengerWorld(World):
|
||||
shard_count = int(item.name.strip("Time Shard ()"))
|
||||
if remove:
|
||||
shard_count = -shard_count
|
||||
state.prog_items["Shards", self.player] += shard_count
|
||||
state.prog_items[self.player]["Shards"] += shard_count
|
||||
|
||||
return super().collect_item(state, item, remove)
|
||||
|
||||
@@ -72,3 +72,10 @@ what item and what player is receiving the item
|
||||
|
||||
Whenever you have an item pending, the next time you are not in a battle, menu, or dialog box, you will receive a
|
||||
message on screen notifying you of the item and sender, and the item will be added directly to your inventory.
|
||||
|
||||
## Unique Local Commands
|
||||
|
||||
The following commands are only available when using the MMBN3Client to play with Archipelago.
|
||||
|
||||
- `/gba` Check GBA Connection State
|
||||
- `/debug` Toggle the Debug Text overlay in ROM
|
||||
|
||||
@@ -2182,7 +2182,7 @@ def patch_rom(world, rom):
|
||||
'Shadow Temple': ("the \x05\x45Shadow Temple", 'Bongo Bongo', 0x7f, 0xa3),
|
||||
}
|
||||
for dungeon in world.dungeon_mq:
|
||||
if dungeon in ['Gerudo Training Ground', 'Ganons Castle']:
|
||||
if dungeon in ['Thieves Hideout', 'Gerudo Training Ground', 'Ganons Castle']:
|
||||
pass
|
||||
elif dungeon in ['Bottom of the Well', 'Ice Cavern']:
|
||||
dungeon_name, boss_name, compass_id, map_id = dungeon_list[dungeon]
|
||||
|
||||
@@ -227,7 +227,8 @@ def set_shop_rules(ootworld):
|
||||
# The goal is to automatically set item rules based on age requirements in case entrances were shuffled
|
||||
def set_entrances_based_rules(ootworld):
|
||||
|
||||
all_state = ootworld.multiworld.get_all_state(False)
|
||||
all_state = ootworld.get_state_with_complete_itempool()
|
||||
all_state.sweep_for_events(locations=ootworld.get_locations())
|
||||
|
||||
for location in filter(lambda location: location.type == 'Shop', ootworld.get_locations()):
|
||||
# If a shop is not reachable as adult, it can't have Goron Tunic or Zora Tunic as child can't buy these
|
||||
|
||||
@@ -829,8 +829,8 @@ class OOTWorld(World):
|
||||
# Kill unreachable events that can't be gotten even with all items
|
||||
# Make sure to only kill actual internal events, not in-game "events"
|
||||
all_state = self.get_state_with_complete_itempool()
|
||||
all_state.sweep_for_events()
|
||||
all_locations = self.get_locations()
|
||||
all_state.sweep_for_events(locations=all_locations)
|
||||
reachable = self.multiworld.get_reachable_locations(all_state, self.player)
|
||||
unreachable = [loc for loc in all_locations if
|
||||
(loc.internal or loc.type == 'Drop') and loc.event and loc.locked and loc not in reachable]
|
||||
@@ -858,7 +858,7 @@ class OOTWorld(World):
|
||||
state = base_state.copy()
|
||||
for item in self.get_pre_fill_items():
|
||||
self.collect(state, item)
|
||||
state.sweep_for_events(self.get_locations())
|
||||
state.sweep_for_events(locations=self.get_locations())
|
||||
return state
|
||||
|
||||
# Prefill shops, songs, and dungeon items
|
||||
@@ -870,7 +870,7 @@ class OOTWorld(World):
|
||||
state = CollectionState(self.multiworld)
|
||||
for item in self.itempool:
|
||||
self.collect(state, item)
|
||||
state.sweep_for_events(self.get_locations())
|
||||
state.sweep_for_events(locations=self.get_locations())
|
||||
|
||||
# Place dungeon items
|
||||
special_fill_types = ['GanonBossKey', 'BossKey', 'SmallKey', 'HideoutSmallKey', 'Map', 'Compass']
|
||||
@@ -1260,16 +1260,16 @@ class OOTWorld(World):
|
||||
def collect(self, state: CollectionState, item: OOTItem) -> bool:
|
||||
if item.advancement and item.special and item.special.get('alias', False):
|
||||
alt_item_name, count = item.special.get('alias')
|
||||
state.prog_items[alt_item_name, self.player] += count
|
||||
state.prog_items[self.player][alt_item_name] += count
|
||||
return True
|
||||
return super().collect(state, item)
|
||||
|
||||
def remove(self, state: CollectionState, item: OOTItem) -> bool:
|
||||
if item.advancement and item.special and item.special.get('alias', False):
|
||||
alt_item_name, count = item.special.get('alias')
|
||||
state.prog_items[alt_item_name, self.player] -= count
|
||||
if state.prog_items[alt_item_name, self.player] < 1:
|
||||
del (state.prog_items[alt_item_name, self.player])
|
||||
state.prog_items[self.player][alt_item_name] -= count
|
||||
if state.prog_items[self.player][alt_item_name] < 1:
|
||||
del (state.prog_items[self.player][alt_item_name])
|
||||
return True
|
||||
return super().remove(state, item)
|
||||
|
||||
|
||||
@@ -31,3 +31,10 @@ Items belonging to other worlds are represented by the Zelda's Letter item.
|
||||
|
||||
When the player receives an item, Link will hold the item above his head and display it to the world. It's good for
|
||||
business!
|
||||
|
||||
## Unique Local Commands
|
||||
|
||||
The following commands are only available when using the OoTClient to play with Archipelago.
|
||||
|
||||
- `/n64` Check N64 Connection State
|
||||
- `/deathlink` Toggle deathlink from client. Overrides default setting.
|
||||
|
||||
@@ -463,13 +463,17 @@ class PokemonRedBlueWorld(World):
|
||||
locs = {self.multiworld.get_location("Fossil - Choice A", self.player),
|
||||
self.multiworld.get_location("Fossil - Choice B", self.player)}
|
||||
|
||||
for loc in locs:
|
||||
if not self.multiworld.key_items_only[self.player]:
|
||||
rule = None
|
||||
if self.multiworld.fossil_check_item_types[self.player] == "key_items":
|
||||
add_item_rule(loc, lambda i: i.advancement)
|
||||
rule = lambda i: i.advancement
|
||||
elif self.multiworld.fossil_check_item_types[self.player] == "unique_items":
|
||||
add_item_rule(loc, lambda i: i.name in item_groups["Unique"])
|
||||
rule = lambda i: i.name in item_groups["Unique"]
|
||||
elif self.multiworld.fossil_check_item_types[self.player] == "no_key_items":
|
||||
add_item_rule(loc, lambda i: not i.advancement)
|
||||
rule = lambda i: not i.advancement
|
||||
if rule:
|
||||
for loc in locs:
|
||||
add_item_rule(loc, rule)
|
||||
|
||||
for mon in ([" ".join(self.multiworld.get_location(
|
||||
f"Oak's Lab - Starter {i}", self.player).item.name.split(" ")[1:]) for i in range(1, 4)]
|
||||
|
||||
@@ -80,3 +80,9 @@ All items for other games will display simply as "AP ITEM," including those for
|
||||
A "received item" sound effect will play. Currently, there is no in-game message informing you of what the item is.
|
||||
If you are in battle, have menus or text boxes opened, or scripted events are occurring, the items will not be given to
|
||||
you until these have ended.
|
||||
|
||||
## Unique Local Commands
|
||||
|
||||
The following command is only available when using the PokemonClient to play with Archipelago.
|
||||
|
||||
- `/gb` Check Gameboy Connection State
|
||||
|
||||
@@ -16,7 +16,7 @@ class Goal(Choice):
|
||||
display_name = "Game Mode"
|
||||
option_classic = 0
|
||||
option_explore = 1
|
||||
default = 0
|
||||
default = 1
|
||||
|
||||
|
||||
class TotalLocations(Range):
|
||||
@@ -48,7 +48,8 @@ class ScavengersPerEnvironment(Range):
|
||||
display_name = "Scavenger per Environment"
|
||||
range_start = 0
|
||||
range_end = 1
|
||||
default = 1
|
||||
default = 0
|
||||
|
||||
|
||||
class ScannersPerEnvironment(Range):
|
||||
"""Explore Mode: The number of scanners locations per environment."""
|
||||
@@ -57,6 +58,7 @@ class ScannersPerEnvironment(Range):
|
||||
range_end = 1
|
||||
default = 1
|
||||
|
||||
|
||||
class AltarsPerEnvironment(Range):
|
||||
"""Explore Mode: The number of altars locations per environment."""
|
||||
display_name = "Newts Per Environment"
|
||||
@@ -64,6 +66,7 @@ class AltarsPerEnvironment(Range):
|
||||
range_end = 2
|
||||
default = 1
|
||||
|
||||
|
||||
class TotalRevivals(Range):
|
||||
"""Total Percentage of `Dio's Best Friend` item put in the item pool."""
|
||||
display_name = "Total Revives as percentage"
|
||||
@@ -83,6 +86,7 @@ class ItemPickupStep(Range):
|
||||
range_end = 5
|
||||
default = 1
|
||||
|
||||
|
||||
class ShrineUseStep(Range):
|
||||
"""
|
||||
Explore Mode:
|
||||
@@ -131,7 +135,6 @@ class DLC_SOTV(Toggle):
|
||||
display_name = "Enable DLC - SOTV"
|
||||
|
||||
|
||||
|
||||
class GreenScrap(Range):
|
||||
"""Weight of Green Scraps in the item pool.
|
||||
|
||||
@@ -274,25 +277,8 @@ class ItemWeights(Choice):
|
||||
option_void = 9
|
||||
|
||||
|
||||
|
||||
|
||||
# define a class for the weights of the generated item pool.
|
||||
@dataclass
|
||||
class ROR2Weights:
|
||||
green_scrap: GreenScrap
|
||||
red_scrap: RedScrap
|
||||
yellow_scrap: YellowScrap
|
||||
white_scrap: WhiteScrap
|
||||
common_item: CommonItem
|
||||
uncommon_item: UncommonItem
|
||||
legendary_item: LegendaryItem
|
||||
boss_item: BossItem
|
||||
lunar_item: LunarItem
|
||||
void_item: VoidItem
|
||||
equipment: Equipment
|
||||
|
||||
@dataclass
|
||||
class ROR2Options(PerGameCommonOptions, ROR2Weights):
|
||||
class ROR2Options(PerGameCommonOptions):
|
||||
goal: Goal
|
||||
total_locations: TotalLocations
|
||||
chests_per_stage: ChestsPerEnvironment
|
||||
@@ -310,4 +296,16 @@ class ROR2Options(PerGameCommonOptions, ROR2Weights):
|
||||
shrine_use_step: ShrineUseStep
|
||||
enable_lunar: AllowLunarItems
|
||||
item_weights: ItemWeights
|
||||
item_pool_presets: ItemPoolPresetToggle
|
||||
item_pool_presets: ItemPoolPresetToggle
|
||||
# define the weights of the generated item pool.
|
||||
green_scrap: GreenScrap
|
||||
red_scrap: RedScrap
|
||||
yellow_scrap: YellowScrap
|
||||
white_scrap: WhiteScrap
|
||||
common_item: CommonItem
|
||||
uncommon_item: UncommonItem
|
||||
legendary_item: LegendaryItem
|
||||
boss_item: BossItem
|
||||
lunar_item: LunarItem
|
||||
void_item: VoidItem
|
||||
equipment: Equipment
|
||||
|
||||
@@ -31,4 +31,24 @@ The goal is to beat the final mission: 'All In'. The config file determines whic
|
||||
|
||||
By default, any of StarCraft 2's items (specified above) can be in another player's world. See the
|
||||
[Advanced YAML Guide](https://archipelago.gg/tutorial/Archipelago/advanced_settings/en)
|
||||
for more information on how to change this.
|
||||
for more information on how to change this.
|
||||
|
||||
## Unique Local Commands
|
||||
|
||||
The following commands are only available when using the Starcraft 2 Client to play with Archipelago.
|
||||
|
||||
- `/difficulty [difficulty]` Overrides the difficulty set for the world.
|
||||
- Options: casual, normal, hard, brutal
|
||||
- `/game_speed [game_speed]` Overrides the game speed for the world
|
||||
- Options: default, slower, slow, normal, fast, faster
|
||||
- `/color [color]` Changes your color (Currently has no effect)
|
||||
- Options: white, red, blue, teal, purple, yellow, orange, green, lightpink, violet, lightgrey, darkgreen, brown,
|
||||
lightgreen, darkgrey, pink, rainbow, random, default
|
||||
- `/disable_mission_check` Disables the check to see if a mission is available to play. Meant for co-op runs where one
|
||||
player can play the next mission in a chain the other player is doing.
|
||||
- `/play [mission_id]` Starts a Starcraft 2 mission based off of the mission_id provided
|
||||
- `/available` Get what missions are currently available to play
|
||||
- `/unfinished` Get what missions are currently available to play and have not had all locations checked
|
||||
- `/set_path [path]` Menually set the SC2 install directory (if the automatic detection fails)
|
||||
- `/download_data` Download the most recent release of the necassry files for playing SC2 with Archipelago. Will
|
||||
overwrite existing files
|
||||
|
||||
@@ -112,15 +112,12 @@ class SMWorld(World):
|
||||
required_client_version = (0, 2, 6)
|
||||
|
||||
itemManager: ItemManager
|
||||
spheres = None
|
||||
|
||||
Logic.factory('vanilla')
|
||||
|
||||
def __init__(self, world: MultiWorld, player: int):
|
||||
self.rom_name_available_event = threading.Event()
|
||||
self.locations = {}
|
||||
if SMWorld.spheres != None:
|
||||
SMWorld.spheres = None
|
||||
super().__init__(world, player)
|
||||
|
||||
@classmethod
|
||||
@@ -368,7 +365,7 @@ class SMWorld(World):
|
||||
locationsDict[first_local_collected_loc.name]),
|
||||
itemLoc.item.player,
|
||||
True)
|
||||
for itemLoc in SMWorld.spheres if itemLoc.item.player == self.player and (not progression_only or itemLoc.item.advancement)
|
||||
for itemLoc in spheres if itemLoc.item.player == self.player and (not progression_only or itemLoc.item.advancement)
|
||||
]
|
||||
|
||||
# Having a sorted itemLocs from collection order is required for escapeTrigger when Tourian is Disabled.
|
||||
@@ -376,8 +373,10 @@ class SMWorld(World):
|
||||
# get_spheres could be cached in multiworld?
|
||||
# Another possible solution would be to have a globally accessible list of items in the order in which the get placed in push_item
|
||||
# and use the inversed starting from the first progression item.
|
||||
if (SMWorld.spheres == None):
|
||||
SMWorld.spheres = [itemLoc for sphere in self.multiworld.get_spheres() for itemLoc in sorted(sphere, key=lambda location: location.name)]
|
||||
spheres: List[Location] = getattr(self.multiworld, "_sm_spheres", None)
|
||||
if spheres is None:
|
||||
spheres = [itemLoc for sphere in self.multiworld.get_spheres() for itemLoc in sorted(sphere, key=lambda location: location.name)]
|
||||
setattr(self.multiworld, "_sm_spheres", spheres)
|
||||
|
||||
self.itemLocs = [
|
||||
ItemLocation(copy.copy(ItemManager.Items[itemLoc.item.type
|
||||
@@ -390,7 +389,7 @@ class SMWorld(World):
|
||||
escapeTrigger = None
|
||||
if self.variaRando.randoExec.randoSettings.restrictions["EscapeTrigger"]:
|
||||
#used to simulate received items
|
||||
first_local_collected_loc = next(itemLoc for itemLoc in SMWorld.spheres if itemLoc.player == self.player)
|
||||
first_local_collected_loc = next(itemLoc for itemLoc in spheres if itemLoc.player == self.player)
|
||||
|
||||
playerItemsItemLocs = get_player_ItemLocation(False)
|
||||
playerProgItemsItemLocs = get_player_ItemLocation(True)
|
||||
|
||||
@@ -470,7 +470,7 @@ class SMZ3World(World):
|
||||
def collect(self, state: CollectionState, item: Item) -> bool:
|
||||
state.smz3state[self.player].Add([TotalSMZ3Item.Item(TotalSMZ3Item.ItemType[item.name], self.smz3World if hasattr(self, "smz3World") else None)])
|
||||
if item.advancement:
|
||||
state.prog_items[item.name, item.player] += 1
|
||||
state.prog_items[item.player][item.name] += 1
|
||||
return True # indicate that a logical state change has occured
|
||||
return False
|
||||
|
||||
@@ -478,9 +478,9 @@ class SMZ3World(World):
|
||||
name = self.collect_item(state, item, True)
|
||||
if name:
|
||||
state.smz3state[item.player].Remove([TotalSMZ3Item.Item(TotalSMZ3Item.ItemType[item.name], self.smz3World if hasattr(self, "smz3World") else None)])
|
||||
state.prog_items[name, item.player] -= 1
|
||||
if state.prog_items[name, item.player] < 1:
|
||||
del (state.prog_items[name, item.player])
|
||||
state.prog_items[item.player][item.name] -= 1
|
||||
if state.prog_items[item.player][item.name] < 1:
|
||||
del (state.prog_items[item.player][item.name])
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ class TestProgressiveToolsLogic(SVTestBase):
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.multiworld.state.prog_items = Counter()
|
||||
self.multiworld.state.prog_items = {1: Counter()}
|
||||
|
||||
def test_sturgeon(self):
|
||||
self.assertFalse(self.world.logic.has("Sturgeon")(self.multiworld.state))
|
||||
|
||||
@@ -31,6 +31,8 @@ highly recommended to use utility mods and features to speed up gameplay, such a
|
||||
- (Can be used to break progression)
|
||||
- Reduced Grinding
|
||||
- Upgraded Research
|
||||
- (WARNING: Do not use without Journey mode)
|
||||
- (NOTE: If items you pick up aren't showing up in your inventory, check your research menu. This mod automatically researches certain items.)
|
||||
|
||||
## Configuring your YAML File
|
||||
|
||||
|
||||
@@ -35,9 +35,17 @@ filler and useful items will cost less, and uncategorized items will be in the m
|
||||
|
||||
## Are there any other changes made?
|
||||
|
||||
- The map and compass for each dungeon start already acquired, and other items can be found in their place.
|
||||
- The map and compass for each dungeon start already acquired, and other items can be found in their place.
|
||||
- The Recorder will warp you between all eight levels regardless of Triforce count
|
||||
- It's possible for this to be your route to level 4!
|
||||
- It's possible for this to be your route to level 4!
|
||||
- Pressing Select will cycle through your inventory.
|
||||
- Shop purchases are tracked within sessions, indicated by the item being elevated from its normal position.
|
||||
- What slots from a Take Any Cave have been chosen are similarly tracked.
|
||||
- What slots from a Take Any Cave have been chosen are similarly tracked.
|
||||
-
|
||||
|
||||
## Local Unique Commands
|
||||
|
||||
The following commands are only available when using the Zelda1Client to play with Archipelago.
|
||||
|
||||
- `/nes` Check NES Connection State
|
||||
- `/toggle_msgs` Toggle displaying messages in EmuHawk
|
||||
|
||||
@@ -42,11 +42,22 @@ In the Pacifist run, you are not required to go to the Ruins to spare Toriel. Th
|
||||
Undyne, and Mettaton EX. Just as it is in the vanilla game, you cannot kill anyone. You are also required to complete
|
||||
the date/hangout with Papyrus, Undyne, and Alphys, in that order, before entering the True Lab.
|
||||
|
||||
Additionally, custom items are required to hang out with Papyrus, Undyne, to enter the True Lab, and to fight
|
||||
Mettaton EX/NEO. The respective items for each interaction are `Complete Skeleton`, `Fish`, `DT Extractor`,
|
||||
Additionally, custom items are required to hang out with Papyrus, Undyne, to enter the True Lab, and to fight
|
||||
Mettaton EX/NEO. The respective items for each interaction are `Complete Skeleton`, `Fish`, `DT Extractor`,
|
||||
and `Mettaton Plush`.
|
||||
|
||||
The Riverperson will only take you to locations you have seen them at, meaning they will only take you to
|
||||
The Riverperson will only take you to locations you have seen them at, meaning they will only take you to
|
||||
Waterfall if you have seen them at Waterfall at least once.
|
||||
|
||||
If you press `W` while in the save menu, you will teleport back to the flower room, for quick access to the other areas.
|
||||
If you press `W` while in the save menu, you will teleport back to the flower room, for quick access to the other areas.
|
||||
|
||||
## Unique Local Commands
|
||||
|
||||
The following commands are only available when using the UndertaleClient to play with Archipelago.
|
||||
|
||||
- `/resync` Manually trigger a resync.
|
||||
- `/patch` Patch the game.
|
||||
- `/savepath` Redirect to proper save data folder. (Use before connecting!)
|
||||
- `/auto_patch` Patch the game automatically.
|
||||
- `/online` Makes you no longer able to see other Undertale players.
|
||||
- `/deathlink` Toggles deathlink
|
||||
|
||||
@@ -26,9 +26,16 @@ Any of the above items can be in another player's world.
|
||||
|
||||
## When the player receives an item, what happens?
|
||||
|
||||
When the player receives an item, a message will appear in Wargroove with the item name and sender name, once an action
|
||||
When the player receives an item, a message will appear in Wargroove with the item name and sender name, once an action
|
||||
is taken in game.
|
||||
|
||||
## What is the goal of this game when randomized?
|
||||
|
||||
The goal is to beat the level titled `The End` by finding the `Final Bridges`, `Final Walls`, and `Final Sickle`.
|
||||
|
||||
## Unique Local Commands
|
||||
|
||||
The following commands are only available when using the WargrooveClient to play with Archipelago.
|
||||
|
||||
- `/resync` Manually trigger a resync.
|
||||
- `/commander` Set the current commander to the given commander.
|
||||
|
||||
@@ -67,8 +67,16 @@ Note that in "restrictive" mode, Champ is the only one that can get Zillion powe
|
||||
|
||||
Canisters retain their original appearance, so you won't know if an item belongs to another player until you collect it.
|
||||
|
||||
When you collect an item, you see the name of the player it goes to. You can see in the client log what item was collected.
|
||||
When you collect an item, you see the name of the player it goes to. You can see in the client log what item was
|
||||
collected.
|
||||
|
||||
## When the player receives an item, what happens?
|
||||
|
||||
The item collect sound is played. You can see in the client log what item was received.
|
||||
|
||||
## Unique Local Commands
|
||||
|
||||
The following commands are only available when using the ZillionClient to play with Archipelago.
|
||||
|
||||
- `/sms` Tell the client that Zillion is running in RetroArch.
|
||||
- `/map` Toggle view of the map tracker.
|
||||
|
||||
Reference in New Issue
Block a user