Merge branch 'ArchipelagoMW:main' into new-options-api

This commit is contained in:
CookieCat
2023-11-04 14:38:16 -04:00
committed by GitHub
45 changed files with 481 additions and 360 deletions

View File

@@ -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:
"""

View File

@@ -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)

View File

@@ -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

View File

@@ -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)

View File

@@ -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=(',', ': '))

View File

@@ -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');

View File

@@ -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;
}

View File

@@ -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 -%}

View File

@@ -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>

View File

@@ -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:

View File

@@ -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)

View File

@@ -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"""

View 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
View 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

View File

@@ -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

View File

@@ -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():

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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):

View File

@@ -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.

View File

@@ -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):

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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):

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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]

View File

@@ -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

View File

@@ -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)

View File

@@ -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.

View File

@@ -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)]

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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))

View File

@@ -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

View 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

View File

@@ -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

View File

@@ -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.

View File

@@ -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.