Merge remote-tracking branch 'Main/main' into rework_accessibility
# Conflicts: # worlds/oot/Rules.py
@@ -328,11 +328,6 @@ class MultiWorld():
|
||||
""" the base name (without file extension) for each player's output file for a seed """
|
||||
return f"AP_{self.seed_name}_P{player}_{self.get_file_safe_player_name(player).replace(' ', '_')}"
|
||||
|
||||
def initialize_regions(self, regions=None):
|
||||
for region in regions if regions else self.regions:
|
||||
region.multiworld = self
|
||||
self._region_cache[region.player][region.name] = region
|
||||
|
||||
@functools.cached_property
|
||||
def world_name_lookup(self):
|
||||
return {self.player_name[player_id]: player_id for player_id in self.player_ids}
|
||||
|
||||
@@ -882,7 +882,7 @@ def get_base_parser(description: typing.Optional[str] = None):
|
||||
def run_as_textclient():
|
||||
class TextContext(CommonContext):
|
||||
# Text Mode to use !hint and such with games that have no text entry
|
||||
tags = {"AP", "TextOnly"}
|
||||
tags = CommonContext.tags | {"TextOnly"}
|
||||
game = "" # empty matches any game since 0.3.2
|
||||
items_handling = 0b111 # receive all items for /received
|
||||
want_slot_data = False # Can't use game specific slot_data
|
||||
|
||||
25
Fill.py
@@ -847,7 +847,7 @@ def distribute_planned(world: MultiWorld) -> None:
|
||||
for target_player in worlds:
|
||||
locations += non_early_locations[target_player]
|
||||
|
||||
block['locations'] = locations
|
||||
block['locations'] = list(dict.fromkeys(locations))
|
||||
|
||||
if not block['count']:
|
||||
block['count'] = (min(len(block['items']), len(block['locations'])) if
|
||||
@@ -897,19 +897,22 @@ def distribute_planned(world: MultiWorld) -> None:
|
||||
for item_name in items:
|
||||
item = world.worlds[player].create_item(item_name)
|
||||
for location in reversed(candidates):
|
||||
if not location.item:
|
||||
if location.item_rule(item):
|
||||
if location.can_fill(world.state, item, False):
|
||||
successful_pairs.append((item, location))
|
||||
candidates.remove(location)
|
||||
count = count + 1
|
||||
break
|
||||
if (location.address is None) == (item.code is None): # either both None or both not None
|
||||
if not location.item:
|
||||
if location.item_rule(item):
|
||||
if location.can_fill(world.state, item, False):
|
||||
successful_pairs.append((item, location))
|
||||
candidates.remove(location)
|
||||
count = count + 1
|
||||
break
|
||||
else:
|
||||
err.append(f"Can't place item at {location} due to fill condition not met.")
|
||||
else:
|
||||
err.append(f"Can't place item at {location} due to fill condition not met.")
|
||||
err.append(f"{item_name} not allowed at {location}.")
|
||||
else:
|
||||
err.append(f"{item_name} not allowed at {location}.")
|
||||
err.append(f"Cannot place {item_name} into already filled location {location}.")
|
||||
else:
|
||||
err.append(f"Cannot place {item_name} into already filled location {location}.")
|
||||
err.append(f"Mismatch between {item_name} and {location}, only one is an event.")
|
||||
if count == maxcount:
|
||||
break
|
||||
if count < placement['count']['min']:
|
||||
|
||||
@@ -169,7 +169,7 @@ def main(args=None, callback=ERmain):
|
||||
for player in range(1, args.multi + 1):
|
||||
player_path_cache[player] = player_files.get(player, args.weights_file_path)
|
||||
name_counter = Counter()
|
||||
erargs.player_settings = {}
|
||||
erargs.player_options = {}
|
||||
|
||||
player = 1
|
||||
while player <= args.multi:
|
||||
|
||||
11
Main.py
@@ -301,15 +301,16 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
||||
|
||||
output = tempfile.TemporaryDirectory()
|
||||
with output as temp_dir:
|
||||
with concurrent.futures.ThreadPoolExecutor(world.players + 2) as pool:
|
||||
output_players = [player for player in world.player_ids if AutoWorld.World.generate_output.__code__
|
||||
is not world.worlds[player].generate_output.__code__]
|
||||
with concurrent.futures.ThreadPoolExecutor(len(output_players) + 2) as pool:
|
||||
check_accessibility_task = pool.submit(world.fulfills_accessibility)
|
||||
|
||||
output_file_futures = [pool.submit(AutoWorld.call_stage, world, "generate_output", temp_dir)]
|
||||
for player in world.player_ids:
|
||||
for player in output_players:
|
||||
# skip starting a thread for methods that say "pass".
|
||||
if AutoWorld.World.generate_output.__code__ is not world.worlds[player].generate_output.__code__:
|
||||
output_file_futures.append(
|
||||
pool.submit(AutoWorld.call_single, world, "generate_output", player, temp_dir))
|
||||
output_file_futures.append(
|
||||
pool.submit(AutoWorld.call_single, world, "generate_output", player, temp_dir))
|
||||
|
||||
# collect ER hint info
|
||||
er_hint_data: Dict[int, Dict[int, str]] = {}
|
||||
|
||||
@@ -67,14 +67,23 @@ def update(yes=False, force=False):
|
||||
install_pkg_resources(yes=yes)
|
||||
import pkg_resources
|
||||
|
||||
prev = "" # if a line ends in \ we store here and merge later
|
||||
for req_file in requirements_files:
|
||||
path = os.path.join(os.path.dirname(sys.argv[0]), req_file)
|
||||
if not os.path.exists(path):
|
||||
path = os.path.join(os.path.dirname(__file__), req_file)
|
||||
with open(path) as requirementsfile:
|
||||
for line in requirementsfile:
|
||||
if not line or line[0] == "#":
|
||||
continue # ignore comments
|
||||
if not line or line.lstrip(" \t")[0] == "#":
|
||||
if not prev:
|
||||
continue # ignore comments
|
||||
line = ""
|
||||
elif line.rstrip("\r\n").endswith("\\"):
|
||||
prev = prev + line.rstrip("\r\n")[:-1] + " " # continue on next line
|
||||
continue
|
||||
line = prev + line
|
||||
line = line.split("--hash=")[0] # remove hashes from requirement for version checking
|
||||
prev = ""
|
||||
if line.startswith(("https://", "git+https://")):
|
||||
# extract name and version for url
|
||||
rest = line.split('/')[-1]
|
||||
|
||||
@@ -959,7 +959,10 @@ class CommonOptions(metaclass=OptionsMetaProperty):
|
||||
else:
|
||||
raise ValueError(f"{casing} is invalid casing for as_dict. "
|
||||
"Valid names are 'snake', 'camel', 'pascal', 'kebab'.")
|
||||
option_results[display_name] = getattr(self, option_name).value
|
||||
value = getattr(self, option_name).value
|
||||
if isinstance(value, set):
|
||||
value = sorted(value)
|
||||
option_results[display_name] = value
|
||||
else:
|
||||
raise ValueError(f"{option_name} not found in {tuple(type(self).type_hints)}")
|
||||
return option_results
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import zipfile
|
||||
from typing import *
|
||||
import base64
|
||||
from typing import Union, Dict, Set, Tuple
|
||||
|
||||
from flask import request, flash, redirect, url_for, render_template
|
||||
from markupsafe import Markup
|
||||
@@ -30,7 +31,15 @@ def check():
|
||||
flash(options)
|
||||
else:
|
||||
results, _ = roll_options(options)
|
||||
return render_template("checkResult.html", results=results)
|
||||
if len(options) > 1:
|
||||
# offer combined file back
|
||||
combined_yaml = "---\n".join(f"# original filename: {file_name}\n{file_content.decode('utf-8-sig')}"
|
||||
for file_name, file_content in options.items())
|
||||
combined_yaml = base64.b64encode(combined_yaml.encode("utf-8-sig")).decode()
|
||||
else:
|
||||
combined_yaml = ""
|
||||
return render_template("checkResult.html",
|
||||
results=results, combined_yaml=combined_yaml)
|
||||
return render_template("check.html")
|
||||
|
||||
|
||||
@@ -41,31 +50,32 @@ def mysterycheck():
|
||||
|
||||
def get_yaml_data(files) -> Union[Dict[str, str], str, Markup]:
|
||||
options = {}
|
||||
for file in files:
|
||||
for uploaded_file in files:
|
||||
# if user does not select file, browser also
|
||||
# submit an empty part without filename
|
||||
if file.filename == '':
|
||||
if uploaded_file.filename == '':
|
||||
return 'No selected file'
|
||||
elif file.filename in options:
|
||||
return f'Conflicting files named {file.filename} submitted'
|
||||
elif file and allowed_file(file.filename):
|
||||
if file.filename.endswith(".zip"):
|
||||
elif uploaded_file.filename in options:
|
||||
return f'Conflicting files named {uploaded_file.filename} submitted'
|
||||
elif uploaded_file and allowed_file(uploaded_file.filename):
|
||||
if uploaded_file.filename.endswith(".zip"):
|
||||
|
||||
with zipfile.ZipFile(file, 'r') as zfile:
|
||||
with zipfile.ZipFile(uploaded_file, 'r') as zfile:
|
||||
infolist = zfile.infolist()
|
||||
|
||||
if any(file.filename.endswith(".archipelago") for file in infolist):
|
||||
return Markup("Error: Your .zip file contains an .archipelago file. "
|
||||
'Did you mean to <a href="/uploads">host a game</a>?')
|
||||
'Did you mean to <a href="/uploads">host a game</a>?')
|
||||
|
||||
for file in infolist:
|
||||
if file.filename.endswith(banned_zip_contents):
|
||||
return "Uploaded data contained a rom file, which is likely to contain copyrighted material. " \
|
||||
"Your file was deleted."
|
||||
return ("Uploaded data contained a rom file, "
|
||||
"which is likely to contain copyrighted material. "
|
||||
"Your file was deleted.")
|
||||
elif file.filename.endswith((".yaml", ".json", ".yml", ".txt")):
|
||||
options[file.filename] = zfile.open(file, "r").read()
|
||||
else:
|
||||
options[file.filename] = file.read()
|
||||
options[uploaded_file.filename] = uploaded_file.read()
|
||||
if not options:
|
||||
return "Did not find a .yaml file to process."
|
||||
return options
|
||||
|
||||
@@ -37,17 +37,29 @@ def start_playing():
|
||||
return render_template(f"startPlaying.html")
|
||||
|
||||
|
||||
@app.route('/weighted-settings')
|
||||
@cache.cached()
|
||||
# TODO for back compat. remove around 0.4.5
|
||||
@app.route("/weighted-settings")
|
||||
def weighted_settings():
|
||||
return render_template(f"weighted-settings.html")
|
||||
return redirect("weighted-options", 301)
|
||||
|
||||
|
||||
# Player settings pages
|
||||
@app.route('/games/<string:game>/player-settings')
|
||||
@app.route("/weighted-options")
|
||||
@cache.cached()
|
||||
def player_settings(game):
|
||||
return render_template(f"player-settings.html", game=game, theme=get_world_theme(game))
|
||||
def weighted_options():
|
||||
return render_template("weighted-options.html")
|
||||
|
||||
|
||||
# TODO for back compat. remove around 0.4.5
|
||||
@app.route("/games/<string:game>/player-settings")
|
||||
def player_settings(game: str):
|
||||
return redirect(url_for("player_options", game=game), 301)
|
||||
|
||||
|
||||
# Player options pages
|
||||
@app.route("/games/<string:game>/player-options")
|
||||
@cache.cached()
|
||||
def player_options(game: str):
|
||||
return render_template("player-options.html", game=game, theme=get_world_theme(game))
|
||||
|
||||
|
||||
# Game Info Pages
|
||||
@@ -181,6 +193,6 @@ def get_sitemap():
|
||||
available_games: List[Dict[str, Union[str, bool]]] = []
|
||||
for game, world in AutoWorldRegister.world_types.items():
|
||||
if not world.hidden:
|
||||
has_settings: bool = isinstance(world.web.settings_page, bool) and world.web.settings_page
|
||||
has_settings: bool = isinstance(world.web.options_page, bool) and world.web.options_page
|
||||
available_games.append({ 'title': game, 'has_settings': has_settings })
|
||||
return render_template("siteMap.html", games=available_games)
|
||||
|
||||
@@ -25,7 +25,7 @@ def create():
|
||||
return "Please document me!"
|
||||
return "\n".join(line.strip() for line in option_type.__doc__.split("\n")).strip()
|
||||
|
||||
weighted_settings = {
|
||||
weighted_options = {
|
||||
"baseOptions": {
|
||||
"description": "Generated by https://archipelago.gg/",
|
||||
"name": "Player",
|
||||
@@ -38,8 +38,8 @@ def create():
|
||||
|
||||
all_options: typing.Dict[str, Options.AssembleOptions] = world.options_dataclass.type_hints
|
||||
|
||||
# Generate JSON files for player-settings pages
|
||||
player_settings = {
|
||||
# Generate JSON files for player-options pages
|
||||
player_options = {
|
||||
"baseOptions": {
|
||||
"description": f"Generated by https://archipelago.gg/ for {game_name}",
|
||||
"game": game_name,
|
||||
@@ -117,17 +117,17 @@ def create():
|
||||
}
|
||||
|
||||
else:
|
||||
logging.debug(f"{option} not exported to Web Settings.")
|
||||
logging.debug(f"{option} not exported to Web options.")
|
||||
|
||||
player_settings["gameOptions"] = game_options
|
||||
player_options["gameOptions"] = game_options
|
||||
|
||||
os.makedirs(os.path.join(target_folder, 'player-settings'), exist_ok=True)
|
||||
os.makedirs(os.path.join(target_folder, 'player-options'), exist_ok=True)
|
||||
|
||||
with open(os.path.join(target_folder, 'player-settings', game_name + ".json"), "w") as f:
|
||||
json.dump(player_settings, f, indent=2, separators=(',', ': '))
|
||||
with open(os.path.join(target_folder, 'player-options', game_name + ".json"), "w") as f:
|
||||
json.dump(player_options, f, indent=2, separators=(',', ': '))
|
||||
|
||||
if not world.hidden and world.web.settings_page is True:
|
||||
# Add the random option to Choice, TextChoice, and Toggle settings
|
||||
if not world.hidden and world.web.options_page is True:
|
||||
# Add the random option to Choice, TextChoice, and Toggle options
|
||||
for option in game_options.values():
|
||||
if option["type"] == "select":
|
||||
option["options"].append({"name": "Random", "value": "random"})
|
||||
@@ -135,11 +135,11 @@ def create():
|
||||
if not option["defaultValue"]:
|
||||
option["defaultValue"] = "random"
|
||||
|
||||
weighted_settings["baseOptions"]["game"][game_name] = 0
|
||||
weighted_settings["games"][game_name] = {}
|
||||
weighted_settings["games"][game_name]["gameSettings"] = game_options
|
||||
weighted_settings["games"][game_name]["gameItems"] = tuple(world.item_names)
|
||||
weighted_settings["games"][game_name]["gameLocations"] = tuple(world.location_names)
|
||||
weighted_options["baseOptions"]["game"][game_name] = 0
|
||||
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]["gameLocations"] = tuple(world.location_names)
|
||||
|
||||
with open(os.path.join(target_folder, 'weighted-settings.json'), "w") as f:
|
||||
json.dump(weighted_settings, f, indent=2, separators=(',', ': '))
|
||||
with open(os.path.join(target_folder, 'weighted-options.json'), "w") as f:
|
||||
json.dump(weighted_options, f, indent=2, separators=(',', ': '))
|
||||
|
||||
@@ -1,41 +1,41 @@
|
||||
let gameName = null;
|
||||
|
||||
window.addEventListener('load', () => {
|
||||
gameName = document.getElementById('player-settings').getAttribute('data-game');
|
||||
gameName = document.getElementById('player-options').getAttribute('data-game');
|
||||
|
||||
// Update game name on page
|
||||
document.getElementById('game-name').innerText = gameName;
|
||||
|
||||
fetchSettingData().then((results) => {
|
||||
let settingHash = localStorage.getItem(`${gameName}-hash`);
|
||||
if (!settingHash) {
|
||||
fetchOptionData().then((results) => {
|
||||
let optionHash = localStorage.getItem(`${gameName}-hash`);
|
||||
if (!optionHash) {
|
||||
// If no hash data has been set before, set it now
|
||||
settingHash = md5(JSON.stringify(results));
|
||||
localStorage.setItem(`${gameName}-hash`, settingHash);
|
||||
optionHash = md5(JSON.stringify(results));
|
||||
localStorage.setItem(`${gameName}-hash`, optionHash);
|
||||
localStorage.removeItem(gameName);
|
||||
}
|
||||
|
||||
if (settingHash !== md5(JSON.stringify(results))) {
|
||||
showUserMessage("Your settings are out of date! Click here to update them! Be aware this will reset " +
|
||||
if (optionHash !== md5(JSON.stringify(results))) {
|
||||
showUserMessage("Your options are out of date! Click here to update them! Be aware this will reset " +
|
||||
"them all to default.");
|
||||
document.getElementById('user-message').addEventListener('click', resetSettings);
|
||||
document.getElementById('user-message').addEventListener('click', resetOptions);
|
||||
}
|
||||
|
||||
// Page setup
|
||||
createDefaultSettings(results);
|
||||
createDefaultOptions(results);
|
||||
buildUI(results);
|
||||
adjustHeaderWidth();
|
||||
|
||||
// Event listeners
|
||||
document.getElementById('export-settings').addEventListener('click', () => exportSettings());
|
||||
document.getElementById('export-options').addEventListener('click', () => exportOptions());
|
||||
document.getElementById('generate-race').addEventListener('click', () => generateGame(true));
|
||||
document.getElementById('generate-game').addEventListener('click', () => generateGame());
|
||||
|
||||
// Name input field
|
||||
const playerSettings = JSON.parse(localStorage.getItem(gameName));
|
||||
const playerOptions = JSON.parse(localStorage.getItem(gameName));
|
||||
const nameInput = document.getElementById('player-name');
|
||||
nameInput.addEventListener('keyup', (event) => updateBaseSetting(event));
|
||||
nameInput.value = playerSettings.name;
|
||||
nameInput.addEventListener('keyup', (event) => updateBaseOption(event));
|
||||
nameInput.value = playerOptions.name;
|
||||
}).catch((e) => {
|
||||
console.error(e);
|
||||
const url = new URL(window.location.href);
|
||||
@@ -43,13 +43,13 @@ window.addEventListener('load', () => {
|
||||
})
|
||||
});
|
||||
|
||||
const resetSettings = () => {
|
||||
const resetOptions = () => {
|
||||
localStorage.removeItem(gameName);
|
||||
localStorage.removeItem(`${gameName}-hash`)
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
const fetchSettingData = () => new Promise((resolve, reject) => {
|
||||
const fetchOptionData = () => new Promise((resolve, reject) => {
|
||||
const ajax = new XMLHttpRequest();
|
||||
ajax.onreadystatechange = () => {
|
||||
if (ajax.readyState !== 4) { return; }
|
||||
@@ -60,54 +60,54 @@ const fetchSettingData = () => new Promise((resolve, reject) => {
|
||||
try{ resolve(JSON.parse(ajax.responseText)); }
|
||||
catch(error){ reject(error); }
|
||||
};
|
||||
ajax.open('GET', `${window.location.origin}/static/generated/player-settings/${gameName}.json`, true);
|
||||
ajax.open('GET', `${window.location.origin}/static/generated/player-options/${gameName}.json`, true);
|
||||
ajax.send();
|
||||
});
|
||||
|
||||
const createDefaultSettings = (settingData) => {
|
||||
const createDefaultOptions = (optionData) => {
|
||||
if (!localStorage.getItem(gameName)) {
|
||||
const newSettings = {
|
||||
const newOptions = {
|
||||
[gameName]: {},
|
||||
};
|
||||
for (let baseOption of Object.keys(settingData.baseOptions)){
|
||||
newSettings[baseOption] = settingData.baseOptions[baseOption];
|
||||
for (let baseOption of Object.keys(optionData.baseOptions)){
|
||||
newOptions[baseOption] = optionData.baseOptions[baseOption];
|
||||
}
|
||||
for (let gameOption of Object.keys(settingData.gameOptions)){
|
||||
newSettings[gameName][gameOption] = settingData.gameOptions[gameOption].defaultValue;
|
||||
for (let gameOption of Object.keys(optionData.gameOptions)){
|
||||
newOptions[gameName][gameOption] = optionData.gameOptions[gameOption].defaultValue;
|
||||
}
|
||||
localStorage.setItem(gameName, JSON.stringify(newSettings));
|
||||
localStorage.setItem(gameName, JSON.stringify(newOptions));
|
||||
}
|
||||
};
|
||||
|
||||
const buildUI = (settingData) => {
|
||||
const buildUI = (optionData) => {
|
||||
// Game Options
|
||||
const leftGameOpts = {};
|
||||
const rightGameOpts = {};
|
||||
Object.keys(settingData.gameOptions).forEach((key, index) => {
|
||||
if (index < Object.keys(settingData.gameOptions).length / 2) { leftGameOpts[key] = settingData.gameOptions[key]; }
|
||||
else { rightGameOpts[key] = settingData.gameOptions[key]; }
|
||||
Object.keys(optionData.gameOptions).forEach((key, index) => {
|
||||
if (index < Object.keys(optionData.gameOptions).length / 2) { leftGameOpts[key] = optionData.gameOptions[key]; }
|
||||
else { rightGameOpts[key] = optionData.gameOptions[key]; }
|
||||
});
|
||||
document.getElementById('game-options-left').appendChild(buildOptionsTable(leftGameOpts));
|
||||
document.getElementById('game-options-right').appendChild(buildOptionsTable(rightGameOpts));
|
||||
};
|
||||
|
||||
const buildOptionsTable = (settings, romOpts = false) => {
|
||||
const currentSettings = JSON.parse(localStorage.getItem(gameName));
|
||||
const buildOptionsTable = (options, romOpts = false) => {
|
||||
const currentOptions = JSON.parse(localStorage.getItem(gameName));
|
||||
const table = document.createElement('table');
|
||||
const tbody = document.createElement('tbody');
|
||||
|
||||
Object.keys(settings).forEach((setting) => {
|
||||
Object.keys(options).forEach((option) => {
|
||||
const tr = document.createElement('tr');
|
||||
|
||||
// td Left
|
||||
const tdl = document.createElement('td');
|
||||
const label = document.createElement('label');
|
||||
label.textContent = `${settings[setting].displayName}: `;
|
||||
label.setAttribute('for', setting);
|
||||
label.textContent = `${options[option].displayName}: `;
|
||||
label.setAttribute('for', option);
|
||||
|
||||
const questionSpan = document.createElement('span');
|
||||
questionSpan.classList.add('interactive');
|
||||
questionSpan.setAttribute('data-tooltip', settings[setting].description);
|
||||
questionSpan.setAttribute('data-tooltip', options[option].description);
|
||||
questionSpan.innerText = '(?)';
|
||||
|
||||
label.appendChild(questionSpan);
|
||||
@@ -120,36 +120,36 @@ const buildOptionsTable = (settings, romOpts = false) => {
|
||||
|
||||
const randomButton = document.createElement('button');
|
||||
|
||||
switch(settings[setting].type){
|
||||
switch(options[option].type){
|
||||
case 'select':
|
||||
element = document.createElement('div');
|
||||
element.classList.add('select-container');
|
||||
let select = document.createElement('select');
|
||||
select.setAttribute('id', setting);
|
||||
select.setAttribute('data-key', setting);
|
||||
select.setAttribute('id', option);
|
||||
select.setAttribute('data-key', option);
|
||||
if (romOpts) { select.setAttribute('data-romOpt', '1'); }
|
||||
settings[setting].options.forEach((opt) => {
|
||||
options[option].options.forEach((opt) => {
|
||||
const option = document.createElement('option');
|
||||
option.setAttribute('value', opt.value);
|
||||
option.innerText = opt.name;
|
||||
if ((isNaN(currentSettings[gameName][setting]) &&
|
||||
(parseInt(opt.value, 10) === parseInt(currentSettings[gameName][setting]))) ||
|
||||
(opt.value === currentSettings[gameName][setting]))
|
||||
if ((isNaN(currentOptions[gameName][option]) &&
|
||||
(parseInt(opt.value, 10) === parseInt(currentOptions[gameName][option]))) ||
|
||||
(opt.value === currentOptions[gameName][option]))
|
||||
{
|
||||
option.selected = true;
|
||||
}
|
||||
select.appendChild(option);
|
||||
});
|
||||
select.addEventListener('change', (event) => updateGameSetting(event.target));
|
||||
select.addEventListener('change', (event) => updateGameOption(event.target));
|
||||
element.appendChild(select);
|
||||
|
||||
// Randomize button
|
||||
randomButton.innerText = '🎲';
|
||||
randomButton.classList.add('randomize-button');
|
||||
randomButton.setAttribute('data-key', setting);
|
||||
randomButton.setAttribute('data-key', option);
|
||||
randomButton.setAttribute('data-tooltip', 'Toggle randomization for this option!');
|
||||
randomButton.addEventListener('click', (event) => toggleRandomize(event, select));
|
||||
if (currentSettings[gameName][setting] === 'random') {
|
||||
if (currentOptions[gameName][option] === 'random') {
|
||||
randomButton.classList.add('active');
|
||||
select.disabled = true;
|
||||
}
|
||||
@@ -163,30 +163,30 @@ const buildOptionsTable = (settings, romOpts = false) => {
|
||||
|
||||
let range = document.createElement('input');
|
||||
range.setAttribute('type', 'range');
|
||||
range.setAttribute('data-key', setting);
|
||||
range.setAttribute('min', settings[setting].min);
|
||||
range.setAttribute('max', settings[setting].max);
|
||||
range.value = currentSettings[gameName][setting];
|
||||
range.setAttribute('data-key', option);
|
||||
range.setAttribute('min', options[option].min);
|
||||
range.setAttribute('max', options[option].max);
|
||||
range.value = currentOptions[gameName][option];
|
||||
range.addEventListener('change', (event) => {
|
||||
document.getElementById(`${setting}-value`).innerText = event.target.value;
|
||||
updateGameSetting(event.target);
|
||||
document.getElementById(`${option}-value`).innerText = event.target.value;
|
||||
updateGameOption(event.target);
|
||||
});
|
||||
element.appendChild(range);
|
||||
|
||||
let rangeVal = document.createElement('span');
|
||||
rangeVal.classList.add('range-value');
|
||||
rangeVal.setAttribute('id', `${setting}-value`);
|
||||
rangeVal.innerText = currentSettings[gameName][setting] !== 'random' ?
|
||||
currentSettings[gameName][setting] : settings[setting].defaultValue;
|
||||
rangeVal.setAttribute('id', `${option}-value`);
|
||||
rangeVal.innerText = currentOptions[gameName][option] !== 'random' ?
|
||||
currentOptions[gameName][option] : options[option].defaultValue;
|
||||
element.appendChild(rangeVal);
|
||||
|
||||
// Randomize button
|
||||
randomButton.innerText = '🎲';
|
||||
randomButton.classList.add('randomize-button');
|
||||
randomButton.setAttribute('data-key', setting);
|
||||
randomButton.setAttribute('data-key', option);
|
||||
randomButton.setAttribute('data-tooltip', 'Toggle randomization for this option!');
|
||||
randomButton.addEventListener('click', (event) => toggleRandomize(event, range));
|
||||
if (currentSettings[gameName][setting] === 'random') {
|
||||
if (currentOptions[gameName][option] === 'random') {
|
||||
randomButton.classList.add('active');
|
||||
range.disabled = true;
|
||||
}
|
||||
@@ -200,11 +200,11 @@ const buildOptionsTable = (settings, romOpts = false) => {
|
||||
|
||||
// Build the select element
|
||||
let specialRangeSelect = document.createElement('select');
|
||||
specialRangeSelect.setAttribute('data-key', setting);
|
||||
Object.keys(settings[setting].value_names).forEach((presetName) => {
|
||||
specialRangeSelect.setAttribute('data-key', option);
|
||||
Object.keys(options[option].value_names).forEach((presetName) => {
|
||||
let presetOption = document.createElement('option');
|
||||
presetOption.innerText = presetName;
|
||||
presetOption.value = settings[setting].value_names[presetName];
|
||||
presetOption.value = options[option].value_names[presetName];
|
||||
const words = presetOption.innerText.split("_");
|
||||
for (let i = 0; i < words.length; i++) {
|
||||
words[i] = words[i][0].toUpperCase() + words[i].substring(1);
|
||||
@@ -217,8 +217,8 @@ const buildOptionsTable = (settings, romOpts = false) => {
|
||||
customOption.value = 'custom';
|
||||
customOption.selected = true;
|
||||
specialRangeSelect.appendChild(customOption);
|
||||
if (Object.values(settings[setting].value_names).includes(Number(currentSettings[gameName][setting]))) {
|
||||
specialRangeSelect.value = Number(currentSettings[gameName][setting]);
|
||||
if (Object.values(options[option].value_names).includes(Number(currentOptions[gameName][option]))) {
|
||||
specialRangeSelect.value = Number(currentOptions[gameName][option]);
|
||||
}
|
||||
|
||||
// Build range element
|
||||
@@ -226,17 +226,17 @@ const buildOptionsTable = (settings, romOpts = false) => {
|
||||
specialRangeWrapper.classList.add('special-range-wrapper');
|
||||
let specialRange = document.createElement('input');
|
||||
specialRange.setAttribute('type', 'range');
|
||||
specialRange.setAttribute('data-key', setting);
|
||||
specialRange.setAttribute('min', settings[setting].min);
|
||||
specialRange.setAttribute('max', settings[setting].max);
|
||||
specialRange.value = currentSettings[gameName][setting];
|
||||
specialRange.setAttribute('data-key', option);
|
||||
specialRange.setAttribute('min', options[option].min);
|
||||
specialRange.setAttribute('max', options[option].max);
|
||||
specialRange.value = currentOptions[gameName][option];
|
||||
|
||||
// Build rage value element
|
||||
let specialRangeVal = document.createElement('span');
|
||||
specialRangeVal.classList.add('range-value');
|
||||
specialRangeVal.setAttribute('id', `${setting}-value`);
|
||||
specialRangeVal.innerText = currentSettings[gameName][setting] !== 'random' ?
|
||||
currentSettings[gameName][setting] : settings[setting].defaultValue;
|
||||
specialRangeVal.setAttribute('id', `${option}-value`);
|
||||
specialRangeVal.innerText = currentOptions[gameName][option] !== 'random' ?
|
||||
currentOptions[gameName][option] : options[option].defaultValue;
|
||||
|
||||
// Configure select event listener
|
||||
specialRangeSelect.addEventListener('change', (event) => {
|
||||
@@ -244,18 +244,18 @@ const buildOptionsTable = (settings, romOpts = false) => {
|
||||
|
||||
// Update range slider
|
||||
specialRange.value = event.target.value;
|
||||
document.getElementById(`${setting}-value`).innerText = event.target.value;
|
||||
updateGameSetting(event.target);
|
||||
document.getElementById(`${option}-value`).innerText = event.target.value;
|
||||
updateGameOption(event.target);
|
||||
});
|
||||
|
||||
// Configure range event handler
|
||||
specialRange.addEventListener('change', (event) => {
|
||||
// Update select element
|
||||
specialRangeSelect.value =
|
||||
(Object.values(settings[setting].value_names).includes(parseInt(event.target.value))) ?
|
||||
(Object.values(options[option].value_names).includes(parseInt(event.target.value))) ?
|
||||
parseInt(event.target.value) : 'custom';
|
||||
document.getElementById(`${setting}-value`).innerText = event.target.value;
|
||||
updateGameSetting(event.target);
|
||||
document.getElementById(`${option}-value`).innerText = event.target.value;
|
||||
updateGameOption(event.target);
|
||||
});
|
||||
|
||||
element.appendChild(specialRangeSelect);
|
||||
@@ -266,12 +266,12 @@ const buildOptionsTable = (settings, romOpts = false) => {
|
||||
// Randomize button
|
||||
randomButton.innerText = '🎲';
|
||||
randomButton.classList.add('randomize-button');
|
||||
randomButton.setAttribute('data-key', setting);
|
||||
randomButton.setAttribute('data-key', option);
|
||||
randomButton.setAttribute('data-tooltip', 'Toggle randomization for this option!');
|
||||
randomButton.addEventListener('click', (event) => toggleRandomize(
|
||||
event, specialRange, specialRangeSelect)
|
||||
);
|
||||
if (currentSettings[gameName][setting] === 'random') {
|
||||
if (currentOptions[gameName][option] === 'random') {
|
||||
randomButton.classList.add('active');
|
||||
specialRange.disabled = true;
|
||||
specialRangeSelect.disabled = true;
|
||||
@@ -281,7 +281,7 @@ const buildOptionsTable = (settings, romOpts = false) => {
|
||||
break;
|
||||
|
||||
default:
|
||||
console.error(`Ignoring unknown setting type: ${settings[setting].type} with name ${setting}`);
|
||||
console.error(`Ignoring unknown option type: ${options[option].type} with name ${option}`);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -311,35 +311,35 @@ const toggleRandomize = (event, inputElement, optionalSelectElement = null) => {
|
||||
optionalSelectElement.disabled = true;
|
||||
}
|
||||
}
|
||||
updateGameSetting(active ? inputElement : randomButton);
|
||||
updateGameOption(active ? inputElement : randomButton);
|
||||
};
|
||||
|
||||
const updateBaseSetting = (event) => {
|
||||
const updateBaseOption = (event) => {
|
||||
const options = JSON.parse(localStorage.getItem(gameName));
|
||||
options[event.target.getAttribute('data-key')] = isNaN(event.target.value) ?
|
||||
event.target.value : parseInt(event.target.value);
|
||||
localStorage.setItem(gameName, JSON.stringify(options));
|
||||
};
|
||||
|
||||
const updateGameSetting = (settingElement) => {
|
||||
const updateGameOption = (optionElement) => {
|
||||
const options = JSON.parse(localStorage.getItem(gameName));
|
||||
if (settingElement.classList.contains('randomize-button')) {
|
||||
if (optionElement.classList.contains('randomize-button')) {
|
||||
// If the event passed in is the randomize button, then we know what we must do.
|
||||
options[gameName][settingElement.getAttribute('data-key')] = 'random';
|
||||
options[gameName][optionElement.getAttribute('data-key')] = 'random';
|
||||
} else {
|
||||
options[gameName][settingElement.getAttribute('data-key')] = isNaN(settingElement.value) ?
|
||||
settingElement.value : parseInt(settingElement.value, 10);
|
||||
options[gameName][optionElement.getAttribute('data-key')] = isNaN(optionElement.value) ?
|
||||
optionElement.value : parseInt(optionElement.value, 10);
|
||||
}
|
||||
|
||||
localStorage.setItem(gameName, JSON.stringify(options));
|
||||
};
|
||||
|
||||
const exportSettings = () => {
|
||||
const settings = JSON.parse(localStorage.getItem(gameName));
|
||||
if (!settings.name || settings.name.toLowerCase() === 'player' || settings.name.trim().length === 0) {
|
||||
const exportOptions = () => {
|
||||
const options = JSON.parse(localStorage.getItem(gameName));
|
||||
if (!options.name || options.name.toLowerCase() === 'player' || options.name.trim().length === 0) {
|
||||
return showUserMessage('You must enter a player name!');
|
||||
}
|
||||
const yamlText = jsyaml.safeDump(settings, { noCompatMode: true }).replaceAll(/'(\d+)':/g, (x, y) => `${y}:`);
|
||||
const yamlText = jsyaml.safeDump(options, { noCompatMode: true }).replaceAll(/'(\d+)':/g, (x, y) => `${y}:`);
|
||||
download(`${document.getElementById('player-name').value}.yaml`, yamlText);
|
||||
};
|
||||
|
||||
@@ -355,14 +355,14 @@ const download = (filename, text) => {
|
||||
};
|
||||
|
||||
const generateGame = (raceMode = false) => {
|
||||
const settings = JSON.parse(localStorage.getItem(gameName));
|
||||
if (!settings.name || settings.name.toLowerCase() === 'player' || settings.name.trim().length === 0) {
|
||||
const options = JSON.parse(localStorage.getItem(gameName));
|
||||
if (!options.name || options.name.toLowerCase() === 'player' || options.name.trim().length === 0) {
|
||||
return showUserMessage('You must enter a player name!');
|
||||
}
|
||||
|
||||
axios.post('/api/generate', {
|
||||
weights: { player: settings },
|
||||
presetData: { player: settings },
|
||||
weights: { player: options },
|
||||
presetData: { player: options },
|
||||
playerCount: 1,
|
||||
spoiler: 3,
|
||||
race: raceMode ? '1' : '0',
|
||||
@@ -1,51 +1,32 @@
|
||||
window.addEventListener('load', () => {
|
||||
const gameHeaders = document.getElementsByClassName('collapse-toggle');
|
||||
Array.from(gameHeaders).forEach((header) => {
|
||||
const gameName = header.getAttribute('data-game');
|
||||
header.addEventListener('click', () => {
|
||||
const gameArrow = document.getElementById(`${gameName}-arrow`);
|
||||
const gameInfo = document.getElementById(gameName);
|
||||
if (gameInfo.classList.contains('collapsed')) {
|
||||
gameArrow.innerText = '▼';
|
||||
gameInfo.classList.remove('collapsed');
|
||||
} else {
|
||||
gameArrow.innerText = '▶';
|
||||
gameInfo.classList.add('collapsed');
|
||||
}
|
||||
});
|
||||
});
|
||||
// Add toggle listener to all elements with .collapse-toggle
|
||||
const toggleButtons = document.querySelectorAll('.collapse-toggle');
|
||||
toggleButtons.forEach((e) => e.addEventListener('click', toggleCollapse));
|
||||
|
||||
// Handle game filter input
|
||||
const gameSearch = document.getElementById('game-search');
|
||||
gameSearch.value = '';
|
||||
|
||||
gameSearch.addEventListener('input', (evt) => {
|
||||
if (!evt.target.value.trim()) {
|
||||
// If input is empty, display all collapsed games
|
||||
return Array.from(gameHeaders).forEach((header) => {
|
||||
return toggleButtons.forEach((header) => {
|
||||
header.style.display = null;
|
||||
const gameName = header.getAttribute('data-game');
|
||||
document.getElementById(`${gameName}-arrow`).innerText = '▶';
|
||||
document.getElementById(gameName).classList.add('collapsed');
|
||||
header.firstElementChild.innerText = '▶';
|
||||
header.nextElementSibling.classList.add('collapsed');
|
||||
});
|
||||
}
|
||||
|
||||
// Loop over all the games
|
||||
Array.from(gameHeaders).forEach((header) => {
|
||||
const gameName = header.getAttribute('data-game');
|
||||
const gameArrow = document.getElementById(`${gameName}-arrow`);
|
||||
const gameInfo = document.getElementById(gameName);
|
||||
|
||||
toggleButtons.forEach((header) => {
|
||||
// If the game name includes the search string, display the game. If not, hide it
|
||||
if (gameName.toLowerCase().includes(evt.target.value.toLowerCase())) {
|
||||
if (header.getAttribute('data-game').toLowerCase().includes(evt.target.value.toLowerCase())) {
|
||||
header.style.display = null;
|
||||
gameArrow.innerText = '▼';
|
||||
gameInfo.classList.remove('collapsed');
|
||||
header.firstElementChild.innerText = '▼';
|
||||
header.nextElementSibling.classList.remove('collapsed');
|
||||
} else {
|
||||
console.log(header);
|
||||
header.style.display = 'none';
|
||||
gameArrow.innerText = '▶';
|
||||
gameInfo.classList.add('collapsed');
|
||||
header.firstElementChild.innerText = '▶';
|
||||
header.nextElementSibling.classList.add('collapsed');
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -54,30 +35,30 @@ window.addEventListener('load', () => {
|
||||
document.getElementById('collapse-all').addEventListener('click', collapseAll);
|
||||
});
|
||||
|
||||
const expandAll = () => {
|
||||
const gameHeaders = document.getElementsByClassName('collapse-toggle');
|
||||
// Loop over all the games
|
||||
Array.from(gameHeaders).forEach((header) => {
|
||||
const gameName = header.getAttribute('data-game');
|
||||
const gameArrow = document.getElementById(`${gameName}-arrow`);
|
||||
const gameInfo = document.getElementById(gameName);
|
||||
const toggleCollapse = (evt) => {
|
||||
const gameArrow = evt.target.firstElementChild;
|
||||
const gameInfo = evt.target.nextElementSibling;
|
||||
if (gameInfo.classList.contains('collapsed')) {
|
||||
gameArrow.innerText = '▼';
|
||||
gameInfo.classList.remove('collapsed');
|
||||
} else {
|
||||
gameArrow.innerText = '▶';
|
||||
gameInfo.classList.add('collapsed');
|
||||
}
|
||||
};
|
||||
|
||||
if (header.style.display === 'none') { return; }
|
||||
gameArrow.innerText = '▼';
|
||||
gameInfo.classList.remove('collapsed');
|
||||
});
|
||||
const expandAll = () => {
|
||||
document.querySelectorAll('.collapse-toggle').forEach((header) => {
|
||||
if (header.style.display === 'none') { return; }
|
||||
header.firstElementChild.innerText = '▼';
|
||||
header.nextElementSibling.classList.remove('collapsed');
|
||||
});
|
||||
};
|
||||
|
||||
const collapseAll = () => {
|
||||
const gameHeaders = document.getElementsByClassName('collapse-toggle');
|
||||
// Loop over all the games
|
||||
Array.from(gameHeaders).forEach((header) => {
|
||||
const gameName = header.getAttribute('data-game');
|
||||
const gameArrow = document.getElementById(`${gameName}-arrow`);
|
||||
const gameInfo = document.getElementById(gameName);
|
||||
|
||||
if (header.style.display === 'none') { return; }
|
||||
gameArrow.innerText = '▶';
|
||||
gameInfo.classList.add('collapsed');
|
||||
});
|
||||
document.querySelectorAll('.collapse-toggle').forEach((header) => {
|
||||
if (header.style.display === 'none') { return; }
|
||||
header.firstElementChild.innerText = '▶';
|
||||
header.nextElementSibling.classList.add('collapsed');
|
||||
});
|
||||
};
|
||||
|
||||
1284
WebHostLib/static/assets/weighted-options.js
Normal file
@@ -4,7 +4,7 @@ html{
|
||||
background-size: 650px 650px;
|
||||
}
|
||||
|
||||
#player-settings{
|
||||
#player-options{
|
||||
box-sizing: border-box;
|
||||
max-width: 1024px;
|
||||
margin-left: auto;
|
||||
@@ -15,14 +15,14 @@ html{
|
||||
color: #eeffeb;
|
||||
}
|
||||
|
||||
#player-settings #player-settings-button-row{
|
||||
#player-options #player-options-button-row{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
#player-settings code{
|
||||
#player-options code{
|
||||
background-color: #d9cd8e;
|
||||
border-radius: 4px;
|
||||
padding-left: 0.25rem;
|
||||
@@ -30,7 +30,7 @@ html{
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
#player-settings #user-message{
|
||||
#player-options #user-message{
|
||||
display: none;
|
||||
width: calc(100% - 8px);
|
||||
background-color: #ffe86b;
|
||||
@@ -40,12 +40,12 @@ html{
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#player-settings #user-message.visible{
|
||||
#player-options #user-message.visible{
|
||||
display: block;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#player-settings h1{
|
||||
#player-options h1{
|
||||
font-size: 2.5rem;
|
||||
font-weight: normal;
|
||||
width: 100%;
|
||||
@@ -53,7 +53,7 @@ html{
|
||||
text-shadow: 1px 1px 4px #000000;
|
||||
}
|
||||
|
||||
#player-settings h2{
|
||||
#player-options h2{
|
||||
font-size: 40px;
|
||||
font-weight: normal;
|
||||
width: 100%;
|
||||
@@ -62,22 +62,22 @@ html{
|
||||
text-shadow: 1px 1px 2px #000000;
|
||||
}
|
||||
|
||||
#player-settings h3, #player-settings h4, #player-settings h5, #player-settings h6{
|
||||
#player-options h3, #player-options h4, #player-options h5, #player-options h6{
|
||||
text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
#player-settings input:not([type]){
|
||||
#player-options input:not([type]){
|
||||
border: 1px solid #000000;
|
||||
padding: 3px;
|
||||
border-radius: 3px;
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
#player-settings input:not([type]):focus{
|
||||
#player-options input:not([type]):focus{
|
||||
border: 1px solid #ffffff;
|
||||
}
|
||||
|
||||
#player-settings select{
|
||||
#player-options select{
|
||||
border: 1px solid #000000;
|
||||
padding: 3px;
|
||||
border-radius: 3px;
|
||||
@@ -85,72 +85,72 @@ html{
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
#player-settings #game-options, #player-settings #rom-options{
|
||||
#player-options #game-options, #player-options #rom-options{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
#player-settings .left, #player-settings .right{
|
||||
#player-options .left, #player-options .right{
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
#player-settings .left{
|
||||
#player-options .left{
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
#player-settings .right{
|
||||
#player-options .right{
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
#player-settings table{
|
||||
#player-options table{
|
||||
margin-bottom: 30px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#player-settings table .select-container{
|
||||
#player-options table .select-container{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
#player-settings table .select-container select{
|
||||
#player-options table .select-container select{
|
||||
min-width: 200px;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
#player-settings table select:disabled{
|
||||
#player-options table select:disabled{
|
||||
background-color: lightgray;
|
||||
}
|
||||
|
||||
#player-settings table .range-container{
|
||||
#player-options table .range-container{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
#player-settings table .range-container input[type=range]{
|
||||
#player-options table .range-container input[type=range]{
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
#player-settings table .range-value{
|
||||
#player-options table .range-value{
|
||||
min-width: 20px;
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
|
||||
#player-settings table .special-range-container{
|
||||
#player-options table .special-range-container{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#player-settings table .special-range-wrapper{
|
||||
#player-options table .special-range-wrapper{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
#player-settings table .special-range-wrapper input[type=range]{
|
||||
#player-options table .special-range-wrapper input[type=range]{
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
#player-settings table .randomize-button {
|
||||
#player-options table .randomize-button {
|
||||
max-height: 24px;
|
||||
line-height: 16px;
|
||||
padding: 2px 8px;
|
||||
@@ -160,23 +160,23 @@ html{
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
#player-settings table .randomize-button.active {
|
||||
#player-options table .randomize-button.active {
|
||||
background-color: #ffef00; /* Same as .interactive in globalStyles.css */
|
||||
}
|
||||
|
||||
#player-settings table .randomize-button[data-tooltip]::after {
|
||||
#player-options table .randomize-button[data-tooltip]::after {
|
||||
left: unset;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
#player-settings table label{
|
||||
#player-options table label{
|
||||
display: block;
|
||||
min-width: 200px;
|
||||
margin-right: 4px;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
#player-settings th, #player-settings td{
|
||||
#player-options th, #player-options td{
|
||||
border: none;
|
||||
padding: 3px;
|
||||
font-size: 17px;
|
||||
@@ -184,17 +184,17 @@ html{
|
||||
}
|
||||
|
||||
@media all and (max-width: 1024px) {
|
||||
#player-settings {
|
||||
#player-options {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
#player-settings #game-options{
|
||||
#player-options #game-options{
|
||||
justify-content: flex-start;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
#player-settings .left,
|
||||
#player-settings .right {
|
||||
#player-options .left,
|
||||
#player-options .right {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@@ -18,10 +18,16 @@
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
#games .collapse-toggle{
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#games h2 .collapse-arrow{
|
||||
font-size: 20px;
|
||||
display: inline-block; /* make vertical-align work */
|
||||
padding-bottom: 9px;
|
||||
vertical-align: middle;
|
||||
cursor: pointer;
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
#games p.collapsed{
|
||||
@@ -42,12 +48,12 @@
|
||||
margin-bottom: 7px;
|
||||
}
|
||||
|
||||
#games #page-controls{
|
||||
#games .page-controls{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
#games #page-controls button{
|
||||
#games .page-controls button{
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
@@ -28,6 +28,10 @@
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% if combined_yaml %}
|
||||
<h1>Combined File Download</h1>
|
||||
<p><a href="data:text/yaml;base64,{{ combined_yaml }}" download="combined.yaml">Download</a></p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
{% extends 'pageWrapper.html' %}
|
||||
|
||||
{% block head %}
|
||||
<title>{{ game }} Settings</title>
|
||||
<title>{{ game }} Options</title>
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/markdown.css") }}" />
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/player-settings.css") }}" />
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/player-options.css") }}" />
|
||||
<script type="application/ecmascript" src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/md5.min.js") }}"></script>
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/js-yaml.min.js") }}"></script>
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/player-settings.js") }}"></script>
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/player-options.js") }}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
{% include 'header/'+theme+'Header.html' %}
|
||||
<div id="player-settings" class="markdown" data-game="{{ game }}">
|
||||
<div id="player-options" class="markdown" data-game="{{ game }}">
|
||||
<div id="user-message"></div>
|
||||
<h1><span id="game-name">Player</span> Settings</h1>
|
||||
<h1><span id="game-name">Player</span> Options</h1>
|
||||
<p>Choose the options you would like to play with! You may generate a single-player game from this page,
|
||||
or download a settings file you can use to participate in a MultiWorld.</p>
|
||||
or download an options file you can use to participate in a MultiWorld.</p>
|
||||
|
||||
<p>
|
||||
A more advanced settings configuration for all games can be found on the
|
||||
<a href="/weighted-settings">Weighted Settings</a> page.
|
||||
A more advanced options configuration for all games can be found on the
|
||||
<a href="/weighted-options">Weighted options</a> page.
|
||||
<br />
|
||||
A list of all games you have generated can be found on the <a href="/user-content">User Content Page</a>.
|
||||
<br />
|
||||
@@ -39,8 +39,8 @@
|
||||
<div id="game-options-right" class="right"></div>
|
||||
</div>
|
||||
|
||||
<div id="player-settings-button-row">
|
||||
<button id="export-settings">Export Settings</button>
|
||||
<div id="player-options-button-row">
|
||||
<button id="export-options">Export Options</button>
|
||||
<button id="generate-game">Generate Game</button>
|
||||
<button id="generate-race">Generate Race</button>
|
||||
</div>
|
||||
@@ -24,7 +24,7 @@
|
||||
<li><a href="/games">Supported Games Page</a></li>
|
||||
<li><a href="/tutorial">Tutorials Page</a></li>
|
||||
<li><a href="/user-content">User Content</a></li>
|
||||
<li><a href="/weighted-settings">Weighted Settings Page</a></li>
|
||||
<li><a href="/weighted-options">Weighted Options Page</a></li>
|
||||
<li><a href="{{url_for('stats')}}">Game Statistics</a></li>
|
||||
<li><a href="/glossary/en">Glossary</a></li>
|
||||
</ul>
|
||||
@@ -46,11 +46,11 @@
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
<h2>Game Settings Pages</h2>
|
||||
<h2>Game Options Pages</h2>
|
||||
<ul>
|
||||
{% for game in games | title_sorted %}
|
||||
{% if game['has_settings'] %}
|
||||
<li><a href="{{ url_for('player_settings', game=game['title']) }}">{{ game['title'] }}</a></li>
|
||||
<li><a href="{{ url_for('player_options', game=game['title']) }}">{{ game['title'] }}</a></li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
@@ -5,15 +5,35 @@
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/markdown.css") }}" />
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/supportedGames.css") }}" />
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/supportedGames.js") }}"></script>
|
||||
<noscript>
|
||||
<style>
|
||||
/* always un-collapse all and hide arrow and search bar */
|
||||
.js-only{
|
||||
display: none;
|
||||
}
|
||||
|
||||
#games p.collapsed{
|
||||
display: block;
|
||||
}
|
||||
|
||||
#games h2 .collapse-arrow{
|
||||
display: none;
|
||||
}
|
||||
|
||||
#games .collapse-toggle{
|
||||
cursor: unset;
|
||||
}
|
||||
</style>
|
||||
</noscript>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
{% include 'header/oceanHeader.html' %}
|
||||
<div id="games" class="markdown">
|
||||
<h1>Currently Supported Games</h1>
|
||||
<div>
|
||||
<div class="js-only">
|
||||
<label for="game-search">Search for your game below!</label><br />
|
||||
<div id="page-controls">
|
||||
<div class="page-controls">
|
||||
<input id="game-search" placeholder="Search by title..." autofocus />
|
||||
<button id="expand-all">Expand All</button>
|
||||
<button id="collapse-all">Collapse All</button>
|
||||
@@ -22,21 +42,21 @@
|
||||
{% for game_name in worlds | title_sorted %}
|
||||
{% set world = worlds[game_name] %}
|
||||
<h2 class="collapse-toggle" data-game="{{ game_name }}">
|
||||
<span id="{{ game_name }}-arrow" class="collapse-arrow">▶</span> {{ game_name }}
|
||||
<span class="collapse-arrow">▶</span>{{ game_name }}
|
||||
</h2>
|
||||
<p id="{{ game_name }}" class="collapsed">
|
||||
<p class="collapsed">
|
||||
{{ world.__doc__ | default("No description provided.", true) }}<br />
|
||||
<a href="{{ url_for("game_info", game=game_name, lang="en") }}">Game Page</a>
|
||||
{% if world.web.tutorials %}
|
||||
<span class="link-spacer">|</span>
|
||||
<a href="{{ url_for("tutorial_landing") }}#{{ game_name }}">Setup Guides</a>
|
||||
{% endif %}
|
||||
{% if world.web.settings_page is string %}
|
||||
{% if world.web.options_page is string %}
|
||||
<span class="link-spacer">|</span>
|
||||
<a href="{{ world.web.settings_page }}">Settings Page</a>
|
||||
{% elif world.web.settings_page %}
|
||||
<a href="{{ world.web.settings_page }}">Options Page</a>
|
||||
{% elif world.web.options_page %}
|
||||
<span class="link-spacer">|</span>
|
||||
<a href="{{ url_for("player_settings", game=game_name) }}">Settings Page</a>
|
||||
<a href="{{ url_for("player_options", game=game_name) }}">Options Page</a>
|
||||
{% endif %}
|
||||
{% if world.web.bug_report_page %}
|
||||
<span class="link-spacer">|</span>
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
{% extends 'pageWrapper.html' %}
|
||||
|
||||
{% block head %}
|
||||
<title>{{ game }} Settings</title>
|
||||
<title>{{ game }} Options</title>
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/markdown.css") }}" />
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/weighted-settings.css") }}" />
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/weighted-options.css") }}" />
|
||||
<script type="application/ecmascript" src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/md5.min.js") }}"></script>
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/js-yaml.min.js") }}"></script>
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/weighted-settings.js") }}"></script>
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/weighted-options.js") }}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
{% include 'header/grassHeader.html' %}
|
||||
<div id="weighted-settings" class="markdown" data-game="{{ game }}">
|
||||
<div id="user-message"></div>
|
||||
<h1>Weighted Settings</h1>
|
||||
<p>Weighted Settings allows you to choose how likely a particular option is to be used in game generation.
|
||||
<h1>Weighted Options</h1>
|
||||
<p>Weighted options allow you to choose how likely a particular option is to be used in game generation.
|
||||
The higher an option is weighted, the more likely the option will be chosen. Think of them like
|
||||
entries in a raffle.</p>
|
||||
|
||||
<p>Choose the games and options you would like to play with! You may generate a single-player game from
|
||||
this page, or download a settings file you can use to participate in a MultiWorld.</p>
|
||||
this page, or download an options file you can use to participate in a MultiWorld.</p>
|
||||
|
||||
<p>A list of all games you have generated can be found on the <a href="/user-content">User Content</a>
|
||||
page.</p>
|
||||
@@ -40,7 +40,7 @@
|
||||
</div>
|
||||
|
||||
<div id="weighted-settings-button-row">
|
||||
<button id="export-settings">Export Settings</button>
|
||||
<button id="export-options">Export Options</button>
|
||||
<button id="generate-game">Generate Game</button>
|
||||
<button id="generate-race">Generate Race</button>
|
||||
</div>
|
||||
@@ -1,214 +1,206 @@
|
||||
# How do I add a game to Archipelago?
|
||||
|
||||
|
||||
# How do I add a game to Archipelago?
|
||||
This guide is going to try and be a broad summary of how you can do just that.
|
||||
There are two key steps to incorporating a game into Archipelago:
|
||||
- Game Modification
|
||||
There are two key steps to incorporating a game into Archipelago:
|
||||
|
||||
- Game Modification
|
||||
- Archipelago Server Integration
|
||||
|
||||
Refer to the following documents as well:
|
||||
- [network protocol.md](https://github.com/ArchipelagoMW/Archipelago/blob/main/docs/network%20protocol.md) for network communication between client and server.
|
||||
- [world api.md](https://github.com/ArchipelagoMW/Archipelago/blob/main/docs/world%20api.md) for documentation on server side code and creating a world package.
|
||||
|
||||
- [network protocol.md](/docs/network%20protocol.md) for network communication between client and server.
|
||||
- [world api.md](/docs/world%20api.md) for documentation on server side code and creating a world package.
|
||||
|
||||
# Game Modification
|
||||
One half of the work required to integrate a game into Archipelago is the development of the game client. This is
|
||||
# Game Modification
|
||||
|
||||
One half of the work required to integrate a game into Archipelago is the development of the game client. This is
|
||||
typically done through a modding API or other modification process, described further down.
|
||||
|
||||
As an example, modifications to a game typically include (more on this later):
|
||||
|
||||
- Hooking into when a 'location check' is completed.
|
||||
- Networking with the Archipelago server.
|
||||
- Optionally, UI or HUD updates to show status of the multiworld session or Archipelago server connection.
|
||||
|
||||
In order to determine how to modify a game, refer to the following sections.
|
||||
|
||||
## Engine Identification
|
||||
This is a good way to make the modding process much easier. Being able to identify what engine a game was made in is critical. The first step is to look at a game's files. Let's go over what some game files might look like. It’s important that you be able to see file extensions, so be sure to enable that feature in your file viewer of choice.
|
||||
|
||||
## Engine Identification
|
||||
|
||||
This is a good way to make the modding process much easier. Being able to identify what engine a game was made in is
|
||||
critical. The first step is to look at a game's files. Let's go over what some game files might look like. It’s
|
||||
important that you be able to see file extensions, so be sure to enable that feature in your file viewer of choice.
|
||||
Examples are provided below.
|
||||
|
||||
|
||||
### Creepy Castle
|
||||

|
||||
|
||||
|
||||

|
||||
|
||||
This is the delightful title Creepy Castle, which is a fantastic game that I highly recommend. It’s also your worst-case
|
||||
scenario as a modder. All that’s present here is an executable file and some meta-information that Steam uses. You have
|
||||
basically nothing here to work with. If you want to change this game, the only option you have is to do some pretty nasty
|
||||
disassembly and reverse engineering work, which is outside the scope of this tutorial. Let’s look at some other examples
|
||||
of game releases.
|
||||
scenario as a modder. All that’s present here is an executable file and some meta-information that Steam uses. You have
|
||||
basically nothing here to work with. If you want to change this game, the only option you have is to do some pretty
|
||||
nasty disassembly and reverse engineering work, which is outside the scope of this tutorial. Let’s look at some other
|
||||
examples of game releases.
|
||||
|
||||
### Heavy Bullets
|
||||

|
||||
|
||||
Here’s the release files for another game, Heavy Bullets. We see a .exe file, like expected, and a few more files.
|
||||
“hello.txt” is a text file, which we can quickly skim in any text editor. Many games have them in some form, usually
|
||||
with a name like README.txt, and they may contain information about a game, such as a EULA, terms of service, licensing
|
||||
information, credits, and general info about the game. You usually won’t find anything too helpful here, but it never
|
||||
hurts to check. In this case, it contains some credits and a changelog for the game, so nothing too important.
|
||||
“steam_api.dll” is a file you can safely ignore, it’s just some code used to interface with Steam.
|
||||
The directory “HEAVY_BULLETS_Data”, however, has some good news.
|
||||
|
||||

|
||||
|
||||
Jackpot! It might not be obvious what you’re looking at here, but I can instantly tell from this folder’s contents that
|
||||
what we have is a game made in the Unity Engine. If you look in the sub-folders, you’ll seem some .dll files which affirm
|
||||
our suspicions. Telltale signs for this are directories titled “Managed” and “Mono”, as well as the numbered, extension-less
|
||||
level files and the sharedassets files. We’ll tell you a bit about why seeing a Unity game is such good news later,
|
||||
but for now, this is what one looks like. Also keep your eyes out for an executable with a name like UnityCrashHandler,
|
||||
that’s another dead giveaway.
|
||||
|
||||

|
||||
|
||||
Here’s the release files for another game, Heavy Bullets. We see a .exe file, like expected, and a few more files.
|
||||
“hello.txt” is a text file, which we can quickly skim in any text editor. Many games have them in some form, usually
|
||||
with a name like README.txt, and they may contain information about a game, such as a EULA, terms of service, licensing
|
||||
information, credits, and general info about the game. You usually won’t find anything too helpful here, but it never
|
||||
hurts to check. In this case, it contains some credits and a changelog for the game, so nothing too important.
|
||||
“steam_api.dll” is a file you can safely ignore, it’s just some code used to interface with Steam.
|
||||
The directory “HEAVY_BULLETS_Data”, however, has some good news.
|
||||
|
||||

|
||||
|
||||
Jackpot! It might not be obvious what you’re looking at here, but I can instantly tell from this folder’s contents that
|
||||
what we have is a game made in the Unity Engine. If you look in the sub-folders, you’ll seem some .dll files which
|
||||
affirm our suspicions. Telltale signs for this are directories titled “Managed” and “Mono”, as well as the numbered,
|
||||
extension-less level files and the sharedassets files. If you've identified the game as a Unity game, some useful tools
|
||||
and information to help you on your journey can be found at this
|
||||
[Unity Game Hacking guide.](https://github.com/imadr/Unity-game-hacking)
|
||||
|
||||
### Stardew Valley
|
||||

|
||||
|
||||
This is the game contents of Stardew Valley. A lot more to look at here, but some key takeaways.
|
||||
Notice the .dll files which include “CSharp” in their name. This tells us that the game was made in C#, which is good news.
|
||||
More on that later.
|
||||
|
||||

|
||||
|
||||
This is the game contents of Stardew Valley. A lot more to look at here, but some key takeaways.
|
||||
Notice the .dll files which include “CSharp” in their name. This tells us that the game was made in C#, which is good
|
||||
news. Many games made in C# can be modified using the same tools found in our Unity game hacking toolset; namely BepInEx
|
||||
and MonoMod.
|
||||
|
||||
### Gato Roboto
|
||||

|
||||
|
||||
Our last example is the game Gato Roboto. This game is made in GameMaker, which is another green flag to look out for.
|
||||
The giveaway is the file titled "data.win". This immediately tips us off that this game was made in GameMaker.
|
||||
|
||||
This isn't all you'll ever see looking at game files, but it's a good place to start.
|
||||
As a general rule, the more files a game has out in plain sight, the more you'll be able to change.
|
||||
This especially applies in the case of code or script files - always keep a lookout for anything you can use to your
|
||||
advantage!
|
||||
|
||||
|
||||

|
||||
|
||||
Our last example is the game Gato Roboto. This game is made in GameMaker, which is another green flag to look out for.
|
||||
The giveaway is the file titled "data.win". This immediately tips us off that this game was made in GameMaker. For
|
||||
modifying GameMaker games the [Undertale Mod Tool](https://github.com/krzys-h/UndertaleModTool) is incredibly helpful.
|
||||
|
||||
This isn't all you'll ever see looking at game files, but it's a good place to start.
|
||||
As a general rule, the more files a game has out in plain sight, the more you'll be able to change.
|
||||
This especially applies in the case of code or script files - always keep a lookout for anything you can use to your
|
||||
advantage!
|
||||
|
||||
## Open or Leaked Source Games
|
||||
As a side note, many games have either been made open source, or have had source files leaked at some point.
|
||||
This can be a boon to any would-be modder, for obvious reasons.
|
||||
Always be sure to check - a quick internet search for "(Game) Source Code" might not give results often, but when it
|
||||
does you're going to have a much better time.
|
||||
|
||||
|
||||
As a side note, many games have either been made open source, or have had source files leaked at some point.
|
||||
This can be a boon to any would-be modder, for obvious reasons. Always be sure to check - a quick internet search for
|
||||
"(Game) Source Code" might not give results often, but when it does, you're going to have a much better time.
|
||||
|
||||
Be sure never to distribute source code for games that you decompile or find if you do not have express permission to do
|
||||
so, or to redistribute any materials obtained through similar methods, as this is illegal and unethical.
|
||||
|
||||
## Modifying Release Versions of Games
|
||||
However, for now we'll assume you haven't been so lucky, and have to work with only what’s sitting in your install directory.
|
||||
Some developers are kind enough to deliberately leave you ways to alter their games, like modding tools,
|
||||
but these are often not geared to the kind of work you'll be doing and may not help much.
|
||||
|
||||
As a general rule, any modding tool that lets you write actual code is something worth using.
|
||||
|
||||
## Modifying Release Versions of Games
|
||||
|
||||
However, for now we'll assume you haven't been so lucky, and have to work with only what’s sitting in your install
|
||||
directory. Some developers are kind enough to deliberately leave you ways to alter their games, like modding tools,
|
||||
but these are often not geared to the kind of work you'll be doing and may not help much.
|
||||
|
||||
As a general rule, any modding tool that lets you write actual code is something worth using.
|
||||
|
||||
### Research
|
||||
The first step is to research your game. Even if you've been dealt the worst hand in terms of engine modification,
|
||||
it's possible other motivated parties have concocted useful tools for your game already.
|
||||
Always be sure to search the Internet for the efforts of other modders.
|
||||
|
||||
### Analysis Tools
|
||||
Depending on the game’s underlying engine, there may be some tools you can use either in lieu of or in addition to existing game tools.
|
||||
|
||||
#### [dnSpy](https://github.com/dnSpy/dnSpy/releases)
|
||||
The first tool in your toolbox is dnSpy.
|
||||
dnSpy is useful for opening and modifying code files, like .exe and .dll files, that were made in C#.
|
||||
This won't work for executable files made by other means, and obfuscated code (code which was deliberately made
|
||||
difficult to reverse engineer) will thwart it, but 9 times out of 10 this is exactly what you need.
|
||||
You'll want to avoid opening common library files in dnSpy, as these are unlikely to contain the data you're looking to
|
||||
modify.
|
||||
|
||||
For Unity games, the file you’ll want to open will be the file (Data Folder)/Managed/Assembly-CSharp.dll, as pictured below:
|
||||
|
||||

|
||||
|
||||
This file will contain the data of the actual game.
|
||||
For other C# games, the file you want is usually just the executable itself.
|
||||
|
||||
With dnSpy, you can view the game’s C# code, but the tool isn’t perfect.
|
||||
Although the names of classes, methods, variables, and more will be preserved, code structures may not remain entirely intact. This is because compilers will often subtly rewrite code to be more optimal, so that it works the same as the original code but uses fewer resources. Compiled C# files also lose comments and other documentation.
|
||||
|
||||
#### [UndertaleModTool](https://github.com/krzys-h/UndertaleModTool/releases)
|
||||
This is currently the best tool for modifying games made in GameMaker, and supports games made in both GMS 1 and 2.
|
||||
It allows you to modify code in GML, if the game wasn't made with the wrong compiler (usually something you don't have
|
||||
to worry about).
|
||||
The first step is to research your game. Even if you've been dealt the worst hand in terms of engine modification,
|
||||
it's possible other motivated parties have concocted useful tools for your game already.
|
||||
Always be sure to search the Internet for the efforts of other modders.
|
||||
|
||||
You'll want to open the data.win file, as this is where all the goods are kept.
|
||||
Like dnSpy, you won’t be able to see comments.
|
||||
In addition, you will be able to see and modify many hidden fields on items that GameMaker itself will often hide from
|
||||
creators.
|
||||
### Other helpful tools
|
||||
|
||||
Fonts in particular are notoriously complex, and to add new sprites you may need to modify existing sprite sheets.
|
||||
|
||||
#### [CheatEngine](https://cheatengine.org/)
|
||||
CheatEngine is a tool with a very long and storied history.
|
||||
Be warned that because it performs live modifications to the memory of other processes, it will likely be flagged as
|
||||
malware (because this behavior is most commonly found in malware and rarely used by other programs).
|
||||
If you use CheatEngine, you need to have a deep understanding of how computers work at the nuts and bolts level,
|
||||
including binary data formats, addressing, and assembly language programming.
|
||||
Depending on the game’s underlying engine, there may be some tools you can use either in lieu of or in addition to
|
||||
existing game tools.
|
||||
|
||||
The tool itself is highly complex and even I have not yet charted its expanses.
|
||||
#### [CheatEngine](https://cheatengine.org/)
|
||||
|
||||
CheatEngine is a tool with a very long and storied history.
|
||||
Be warned that because it performs live modifications to the memory of other processes, it will likely be flagged as
|
||||
malware (because this behavior is most commonly found in malware and rarely used by other programs).
|
||||
If you use CheatEngine, you need to have a deep understanding of how computers work at the nuts and bolts level,
|
||||
including binary data formats, addressing, and assembly language programming.
|
||||
|
||||
The tool itself is highly complex and even I have not yet charted its expanses.
|
||||
However, it can also be a very powerful tool in the right hands, allowing you to query and modify gamestate without ever
|
||||
modifying the actual game itself.
|
||||
In theory it is compatible with any piece of software you can run on your computer, but there is no "easy way" to do
|
||||
anything with it.
|
||||
|
||||
### What Modifications You Should Make to the Game
|
||||
We talked about this briefly in [Game Modification](#game-modification) section.
|
||||
The next step is to know what you need to make the game do now that you can modify it. Here are your key goals:
|
||||
- Modify the game so that checks are shuffled
|
||||
- Know when the player has completed a check, and react accordingly
|
||||
- Listen for messages from the Archipelago server
|
||||
- Modify the game to display messages from the Archipelago server
|
||||
- Add interface for connecting to the Archipelago server with passwords and sessions
|
||||
- Add commands for manually rewarding, re-syncing, releasing, and other actions
|
||||
|
||||
To elaborate, you need to be able to inform the server whenever you check locations, print out messages that you receive
|
||||
from the server in-game so players can read them, award items when the server tells you to, sync and re-sync when necessary,
|
||||
avoid double-awarding items while still maintaining game file integrity, and allow players to manually enter commands in
|
||||
case the client or server make mistakes.
|
||||
modifying the actual game itself.
|
||||
In theory it is compatible with any piece of software you can run on your computer, but there is no "easy way" to do
|
||||
anything with it.
|
||||
|
||||
### What Modifications You Should Make to the Game
|
||||
|
||||
We talked about this briefly in [Game Modification](#game-modification) section.
|
||||
The next step is to know what you need to make the game do now that you can modify it. Here are your key goals:
|
||||
|
||||
- Know when the player has checked a location, and react accordingly
|
||||
- Be able to receive items from the server on the fly
|
||||
- Keep an index for items received in order to resync from disconnections
|
||||
- Add interface for connecting to the Archipelago server with passwords and sessions
|
||||
- Add commands for manually rewarding, re-syncing, releasing, and other actions
|
||||
|
||||
Refer to the [Network Protocol documentation](/docs/network%20protocol.md) for how to communicate with Archipelago's
|
||||
servers.
|
||||
|
||||
## But my Game is a console game. Can I still add it?
|
||||
|
||||
That depends – what console?
|
||||
|
||||
### My Game is a recent game for the PS4/Xbox-One/Nintendo Switch/etc
|
||||
|
||||
Refer to the [Network Protocol documentation](./network%20protocol.md) for how to communicate with Archipelago's servers.
|
||||
|
||||
## But my Game is a console game. Can I still add it?
|
||||
That depends – what console?
|
||||
|
||||
### My Game is a recent game for the PS4/Xbox-One/Nintendo Switch/etc
|
||||
Most games for recent generations of console platforms are inaccessible to the typical modder. It is generally advised
|
||||
that you do not attempt to work with these games as they are difficult to modify and are protected by their copyright
|
||||
holders. Most modern AAA game studios will provide a modding interface or otherwise deny modifications for their console games.
|
||||
|
||||
### My Game isn’t that old, it’s for the Wii/PS2/360/etc
|
||||
This is very complex, but doable.
|
||||
If you don't have good knowledge of stuff like Assembly programming, this is not where you want to learn it.
|
||||
holders. Most modern AAA game studios will provide a modding interface or otherwise deny modifications for their console
|
||||
games.
|
||||
|
||||
### My Game isn’t that old, it’s for the Wii/PS2/360/etc
|
||||
|
||||
This is very complex, but doable.
|
||||
If you don't have good knowledge of stuff like Assembly programming, this is not where you want to learn it.
|
||||
There exist many disassembly and debugging tools, but more recent content may have lackluster support.
|
||||
|
||||
### My Game is a classic for the SNES/Sega Genesis/etc
|
||||
That’s a lot more feasible.
|
||||
There are many good tools available for understanding and modifying games on these older consoles, and the emulation
|
||||
community will have figured out the bulk of the console’s secrets.
|
||||
Look for debugging tools, but be ready to learn assembly.
|
||||
Old consoles usually have their own unique dialects of ASM you’ll need to get used to.
|
||||
|
||||
### My Game is a classic for the SNES/Sega Genesis/etc
|
||||
|
||||
That’s a lot more feasible.
|
||||
There are many good tools available for understanding and modifying games on these older consoles, and the emulation
|
||||
community will have figured out the bulk of the console’s secrets.
|
||||
Look for debugging tools, but be ready to learn assembly.
|
||||
Old consoles usually have their own unique dialects of ASM you’ll need to get used to.
|
||||
|
||||
Also make sure there’s a good way to interface with a running emulator, since that’s the only way you can connect these
|
||||
older consoles to the Internet.
|
||||
There are also hardware mods and flash carts, which can do the same things an emulator would when connected to a computer,
|
||||
but these will require the same sort of interface software to be written in order to work properly - from your perspective
|
||||
the two won't really look any different.
|
||||
|
||||
### My Game is an exclusive for the Super Baby Magic Dream Boy. It’s this console from the Soviet Union that-
|
||||
Unless you have a circuit schematic for the Super Baby Magic Dream Boy sitting on your desk, no.
|
||||
There are also hardware mods and flash carts, which can do the same things an emulator would when connected to a
|
||||
computer, but these will require the same sort of interface software to be written in order to work properly; from your
|
||||
perspective the two won't really look any different.
|
||||
|
||||
### My Game is an exclusive for the Super Baby Magic Dream Boy. It’s this console from the Soviet Union that-
|
||||
|
||||
Unless you have a circuit schematic for the Super Baby Magic Dream Boy sitting on your desk, no.
|
||||
Obscurity is your enemy – there will likely be little to no emulator or modding information, and you’d essentially be
|
||||
working from scratch.
|
||||
|
||||
working from scratch.
|
||||
|
||||
## How to Distribute Game Modifications
|
||||
|
||||
**NEVER EVER distribute anyone else's copyrighted work UNLESS THEY EXPLICITLY GIVE YOU PERMISSION TO DO SO!!!**
|
||||
|
||||
This is a good way to get any project you're working on sued out from under you.
|
||||
The right way to distribute modified versions of a game's binaries, assuming that the licensing terms do not allow you
|
||||
to copy them wholesale, is as patches.
|
||||
to copy them wholesale, is as patches.
|
||||
|
||||
There are many patch formats, which I'll cover in brief. The common theme is that you can’t distribute anything that
|
||||
wasn't made by you. Patches are files that describe how your modified file differs from the original one, thus avoiding
|
||||
the issue of distributing someone else’s original work.
|
||||
|
||||
Users who have a copy of the game just need to apply the patch, and those who don’t are unable to play.
|
||||
Users who have a copy of the game just need to apply the patch, and those who don’t are unable to play.
|
||||
|
||||
### Patches
|
||||
|
||||
#### IPS
|
||||
|
||||
IPS patches are a simple list of chunks to replace in the original to generate the output. It is not possible to encode
|
||||
moving of a chunk, so they may inadvertently contain copyrighted material and should be avoided unless you know it's
|
||||
fine.
|
||||
|
||||
#### UPS, BPS, VCDIFF (xdelta), bsdiff
|
||||
|
||||
Other patch formats generate the difference between two streams (delta patches) with varying complexity. This way it is
|
||||
possible to insert bytes or move chunks without including any original data. Bsdiff is highly optimized and includes
|
||||
compression, so this format is used by APBP.
|
||||
@@ -217,6 +209,7 @@ Only a bsdiff module is integrated into AP. If the final patch requires or is ba
|
||||
bsdiff or APBP before adding it to the AP source code as "basepatch.bsdiff4" or "basepatch.apbp".
|
||||
|
||||
#### APBP Archipelago Binary Patch
|
||||
|
||||
Starting with version 4 of the APBP format, this is a ZIP file containing metadata in `archipelago.json` and additional
|
||||
files required by the game / patching process. For ROM-based games the ZIP will include a `delta.bsdiff4` which is the
|
||||
bsdiff between the original and the randomized ROM.
|
||||
@@ -224,121 +217,53 @@ bsdiff between the original and the randomized ROM.
|
||||
To make using APBP easy, they can be generated by inheriting from `worlds.Files.APDeltaPatch`.
|
||||
|
||||
### Mod files
|
||||
|
||||
Games which support modding will usually just let you drag and drop the mod’s files into a folder somewhere.
|
||||
Mod files come in many forms, but the rules about not distributing other people's content remain the same.
|
||||
They can either be generic and modify the game using a seed or `slot_data` from the AP websocket, or they can be
|
||||
generated per seed.
|
||||
generated per seed. If at all possible, it's generally best practice to collect your world information from `slot_data`
|
||||
so that the users don't have to move files around in order to play.
|
||||
|
||||
If the mod is generated by AP and is installed from a ZIP file, it may be possible to include APBP metadata for easy
|
||||
integration into the Webhost by inheriting from `worlds.Files.APContainer`.
|
||||
|
||||
|
||||
## Archipelago Integration
|
||||
Integrating a randomizer into Archipelago involves a few steps.
|
||||
There are several things that may need to be done, but the most important is to create an implementation of the
|
||||
`World` class specific to your game. This implementation should exist as a Python module within the `worlds` folder
|
||||
in the Archipelago file structure.
|
||||
|
||||
This encompasses most of the data for your game – the items available, what checks you have, the logic for reaching those
|
||||
checks, what options to offer for the player’s yaml file, and the code to initialize all this data.
|
||||
In order for your game to communicate with the Archipelago server and generate the necessary randomized information,
|
||||
you must create a world package in the main Archipelago repo. This section will cover the requisites and expectations
|
||||
and show the basics of a world. More in depth documentation on the available API can be read in
|
||||
the [world api doc.](/docs/world%20api.md)
|
||||
For setting up your working environment with Archipelago refer
|
||||
to [running from source](/docs/running%20from%20source.md) and the [style guide](/docs/style.md).
|
||||
|
||||
Here’s an example of what your world module can look like:
|
||||
|
||||

|
||||
### Requirements
|
||||
|
||||
The minimum requirements for a new archipelago world are the package itself (the world folder containing a file named `__init__.py`),
|
||||
which must define a `World` class object for the game with a game name, create an equal number of items and locations with rules,
|
||||
a win condition, and at least one `Region` object.
|
||||
|
||||
Let's give a quick breakdown of what the contents for these files look like.
|
||||
This is just one example of an Archipelago world - the way things are done below is not an immutable property of Archipelago.
|
||||
|
||||
### Items.py
|
||||
This file is used to define the items which exist in a given game.
|
||||
|
||||

|
||||
|
||||
Some important things to note here. The center of our Items.py file is the item_table, which individually lists every
|
||||
item in the game and associates them with an ItemData.
|
||||
A world implementation requires a few key things from its implementation
|
||||
|
||||
This file is rather skeletal - most of the actual data has been stripped out for simplicity.
|
||||
Each ItemData gives a numeric ID to associate with the item and a boolean telling us whether the item might allow the
|
||||
player to do more than they would have been able to before.
|
||||
|
||||
Next there's the item_frequencies. This simply tells Archipelago how many times each item appears in the pool.
|
||||
Items that appear exactly once need not be listed - Archipelago will interpret absence from this dictionary as meaning
|
||||
that the item appears once.
|
||||
|
||||
Lastly, note the `lookup_id_to_name` dictionary, which is typically imported and used in your Archipelago `World`
|
||||
implementation. This is how Archipelago is told about the items in your world.
|
||||
|
||||
### Locations.py
|
||||
This file lists all locations in the game.
|
||||
|
||||

|
||||
|
||||
First is the achievement_table. It lists each location, the region that it can be found in (more on regions later),
|
||||
and a numeric ID to associate with each location.
|
||||
|
||||
The exclusion table is a series of dictionaries which are used to exclude certain checks from the pool of progression
|
||||
locations based on user settings, and the events table associates certain specific checks with specific items.
|
||||
|
||||
`lookup_id_to_name` is also present for locations, though this is a separate dictionary, to be clear.
|
||||
|
||||
### Options.py
|
||||
This file details options to be searched for in a player's YAML settings file.
|
||||
|
||||

|
||||
|
||||
There are several types of option Archipelago has support for.
|
||||
In our case, we have three separate choices a player can toggle, either On or Off.
|
||||
You can also have players choose between a number of predefined values, or have them provide a numeric value within a
|
||||
specified range.
|
||||
|
||||
### Regions.py
|
||||
This file contains data which defines the world's topology.
|
||||
In other words, it details how different regions of the game connect to each other.
|
||||
|
||||

|
||||
|
||||
`terraria_regions` contains a list of tuples.
|
||||
The first element of the tuple is the name of the region, and the second is a list of connections that lead out of the region.
|
||||
|
||||
`mandatory_connections` describe where the connection leads.
|
||||
|
||||
Above this data is a function called `link_terraria_structures` which uses our defined regions and connections to create
|
||||
something more usable for Archipelago, but this has been left out for clarity.
|
||||
|
||||
### Rules.py
|
||||
This is the file that details rules for what players can and cannot logically be required to do, based on items and settings.
|
||||
|
||||

|
||||
|
||||
This is the most complicated part of the job, and is one part of Archipelago that is likely to see some changes in the future.
|
||||
The first class, called `TerrariaLogic`, is an extension of the `LogicMixin` class.
|
||||
This is where you would want to define methods for evaluating certain conditions, which would then return a boolean to
|
||||
indicate whether conditions have been met. Your rule definitions should start with some sort of identifier to delineate it
|
||||
from other games, as all rules are mixed together due to `LogicMixin`. In our case, `_terraria_rule` would be a better name.
|
||||
|
||||
The method below, `set_rules()`, is where you would assign these functions as "rules", using lambdas to associate these
|
||||
functions or combinations of them (or any other code that evaluates to a boolean, in my case just the placeholder `True`)
|
||||
to certain tasks, like checking locations or using entrances.
|
||||
|
||||
### \_\_init\_\_.py
|
||||
This is the file that actually extends the `World` class, and is where you expose functionality and data to Archipelago.
|
||||
|
||||

|
||||
|
||||
This is the most important file for the implementation, and technically the only one you need, but it's best to keep this
|
||||
file as short as possible and use other script files to do most of the heavy lifting.
|
||||
If you've done things well, this will just be where you assign everything you set up in the other files to their associated
|
||||
fields in the class being extended.
|
||||
|
||||
This is also a good place to put game-specific quirky behavior that needs to be managed, as it tends to make things a bit
|
||||
cluttered if you put these things elsewhere.
|
||||
|
||||
The various methods and attributes are documented in `/worlds/AutoWorld.py[World]` and
|
||||
[world api.md](https://github.com/ArchipelagoMW/Archipelago/blob/main/docs/world%20api.md),
|
||||
though it is also recommended to look at existing implementations to see how all this works first-hand.
|
||||
Once you get all that, all that remains to do is test the game and publish your work.
|
||||
Make sure to check out [world maintainer.md](./world%20maintainer.md) before publishing.
|
||||
- A folder within `worlds` that contains an `__init__.py`
|
||||
- This is what defines it as a Python package and how it's able to be imported
|
||||
into Archipelago's generation system. During generation time only code that is
|
||||
defined within this file will be run. It's suggested to split up your information
|
||||
into more files to improve readability, but all of that information can be
|
||||
imported at its base level within your world.
|
||||
- A `World` subclass where you create your world and define all of its rules
|
||||
and the following requirements:
|
||||
- Your items and locations need a `item_name_to_id` and `location_name_to_id`,
|
||||
respectively, mapping.
|
||||
- An `option_definitions` mapping of your game options with the format
|
||||
`{name: Class}`, where `name` uses Python snake_case.
|
||||
- You must define your world's `create_item` method, because this may be called
|
||||
by the generator in certain circumstances
|
||||
- When creating your world you submit items and regions to the Multiworld.
|
||||
- These are lists of said objects which you can access at
|
||||
`self.multiworld.itempool` and `self.multiworld.regions`. Best practice for
|
||||
adding to these lists is with either `append` or `extend`, where `append` is a
|
||||
single object and `extend` is a list.
|
||||
- Do not use `=` as this will delete other worlds' items and regions.
|
||||
- Regions are containers for holding your world's Locations.
|
||||
- Locations are where players will "check" for items and must exist within
|
||||
a region. It's also important for your world's submitted items to be the same as
|
||||
its submitted locations count.
|
||||
- You must always have a "Menu" Region from which the generation algorithm
|
||||
uses to enter the game and access locations.
|
||||
- Make sure to check out [world maintainer.md](/docs/world%20maintainer.md) before publishing.
|
||||
|
Before Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 81 KiB |
|
Before Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 65 KiB |
|
Before Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 83 KiB |
|
Before Width: | Height: | Size: 82 KiB |
100
docs/triage role expectations.md
Normal file
@@ -0,0 +1,100 @@
|
||||
# Triage Role Expectations
|
||||
|
||||
Users with Triage-level access are selected contributors who can and wish to proactively label/triage issues and pull
|
||||
requests without being granted write access to the Archipelago repository.
|
||||
|
||||
Triage users are not necessarily official members of the Archipelago organization, for the list of core maintainers,
|
||||
please reference [ArchipelagoMW Members](https://github.com/orgs/ArchipelagoMW/people) page.
|
||||
|
||||
## Access Permissions
|
||||
|
||||
Triage users have the following permissions:
|
||||
|
||||
* Apply/dismiss labels on all issues and pull requests.
|
||||
* Close, reopen, and assign all issues and pull requests.
|
||||
* Mark issues and pull requests as duplicate.
|
||||
* Request pull request reviews from repository members.
|
||||
* Hide comments in issues or pull requests from public view.
|
||||
* Hidden comments are not deleted and can be reversed by another triage user or repository member with write access.
|
||||
* And all other standard permissions granted to regular GitHub users.
|
||||
|
||||
For more details on permissions granted by the Triage role, see
|
||||
[GitHub's Role Documentation](https://docs.github.com/en/organizations/managing-user-access-to-your-organizations-repositories/managing-repository-roles/repository-roles-for-an-organization).
|
||||
|
||||
## Expectations
|
||||
|
||||
Users with triage-level permissions have no expectation to review code, but, if desired, to review pull requests/issues
|
||||
and apply the relevant labels and ping/request reviews from any relevant [code owners](./CODEOWNERS) for review. Triage
|
||||
users are also expected not to close others' issues or pull requests without strong reason to do so (with exception of
|
||||
`meta: invalid` or `meta: duplicate` scenarios, which are listed below). When in doubt, defer to a core maintainer.
|
||||
|
||||
Triage users are not "moderators" for others' issues or pull requests. However, they may voice their opinions/feedback
|
||||
on issues or pull requests, just the same as any other GitHub user contributing to Archipelago.
|
||||
|
||||
## Labeling
|
||||
|
||||
As of the time of writing this document, there are 15 distinct labels that can be applied to issues and pull requests.
|
||||
|
||||
### Affects
|
||||
|
||||
These labels notate if certain issues or pull requests affect critical aspects of Archipelago that may require specific
|
||||
review. More than one of these labels can be used on a issue or pull request, if relevant.
|
||||
|
||||
* `affects: core` is to be applied to issues/PRs that may affect core Archipelago functionality and should be reviewed
|
||||
with additional scrutiny.
|
||||
* Core is defined as any files not contained in the `WebHostLib` directory or individual world implementations
|
||||
directories inside the `worlds` directory, not including `worlds/generic`.
|
||||
* `affects: webhost` is to be applied to issues/PRs that may affect the core WebHost portion of Archipelago. In
|
||||
general, this is anything being modified inside the `WebHostLib` directory or `WebHost.py` file.
|
||||
* `affects: release/blocker` is to be applied for any issues/PRs that may either negatively impact (issues) or propose
|
||||
to resolve critical issues (pull requests) that affect the current or next official release of Archipelago and should be
|
||||
given top priority for review.
|
||||
|
||||
### Is
|
||||
|
||||
These labels notate what kinds of changes are being made or proposed in issues or pull requests. More than one of these
|
||||
labels can be used on a issue or pull request, if relevant, but at least one of these labels should be applied to every
|
||||
pull request and issue.
|
||||
|
||||
* `is: bug/fix` is to be applied to issues/PRs that report or resolve an issue in core, web, or individual world
|
||||
implementations.
|
||||
* `is: documentation` is to be applied to issues/PRs that relate to adding, updating, or removing documentation in
|
||||
core, web, or individual world implementations without modifying actual code.
|
||||
* `is: enhancement` is to be applied to issues/PRs that relate to adding, modifying, or removing functionality in
|
||||
core, web, or individual world implementations.
|
||||
* `is: refactor/cleanup` is to be applied to issues/PRs that relate to reorganizing existing code to improve
|
||||
readability or performance without adding, modifying, or removing functionality or fixing known regressions.
|
||||
* `is: maintenance` is to be applied to issues/PRs that don't modify logic, refactor existing code, change features.
|
||||
This is typically reserved for pull requests that need to update dependencies or increment version numbers without
|
||||
resolving existing issues.
|
||||
* `is: new game` is to be applied to any pull requests that introduce a new game for the first time to the `worlds`
|
||||
directory.
|
||||
* Issues should not be opened and classified with `is: new game`, and instead should be directed to the
|
||||
#future-game-design channel in Archipelago for opening suggestions. If they are opened, they should be labeled
|
||||
with `meta: invalid` and closed.
|
||||
* Pull requests for new games should only have this label, as enhancement, documentation, bug/fix, refactor, and
|
||||
possibly maintenance is implied.
|
||||
|
||||
### Meta
|
||||
|
||||
These labels allow additional quick meta information for contributors or reviewers for issues and pull requests. They
|
||||
have specific situations where they should be applied.
|
||||
|
||||
* `meta: duplicate` is to be applied to any issues/PRs that are duplicate of another issue/PR that was already opened.
|
||||
* These should be immediately closed after leaving a comment, directing to the original issue or pull request.
|
||||
* `meta: invalid` is to be applied to any issues/PRs that do not relate to Archipelago or are inappropriate for
|
||||
discussion on GitHub.
|
||||
* These should be immediately closed afterwards.
|
||||
* `meta: help wanted` is to be applied to any issues/PRs that require additional attention for whatever reason.
|
||||
* These should include a comment describing what kind of help is requested when the label is added.
|
||||
* Some common reasons include, but are not limited to: Breaking API changes that require developer input/testing or
|
||||
pull requests with large line changes that need additional reviewers to be reviewed effectively.
|
||||
* This label may require some programming experience and familiarity with Archipelago source to determine if
|
||||
requesting additional attention for help is warranted.
|
||||
* `meta: good first issue` is to be applied to any issues that may be a good starting ground for new contributors to try
|
||||
and tackle.
|
||||
* This label may require some programming experience and familiarity with Archipelago source to determine if an
|
||||
issue is a "good first issue".
|
||||
* `meta: wontfix` is to be applied for any issues/PRs that are opened that will not be actioned because it's out of
|
||||
scope or determined to not be an issue.
|
||||
* This should be reserved for use by a world's code owner(s) on their relevant world or by core maintainers.
|
||||
@@ -759,8 +759,9 @@ multiworld for each test written using it. Within subsequent modules, classes sh
|
||||
TestBase, and can then define options to test in the class body, and run tests in each test method.
|
||||
|
||||
Example `__init__.py`
|
||||
|
||||
```python
|
||||
from test.TestBase import WorldTestBase
|
||||
from test.test_base import WorldTestBase
|
||||
|
||||
|
||||
class MyGameTestBase(WorldTestBase):
|
||||
|
||||
871
inno_setup.iss
@@ -46,151 +46,33 @@ Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{
|
||||
|
||||
[Types]
|
||||
Name: "full"; Description: "Full installation"
|
||||
Name: "hosting"; Description: "Installation for hosting purposes"
|
||||
Name: "playing"; Description: "Installation for playing purposes"
|
||||
Name: "minimal"; Description: "Minimal installation"
|
||||
Name: "custom"; Description: "Custom installation"; Flags: iscustom
|
||||
|
||||
[Components]
|
||||
Name: "core"; Description: "Core Files"; Types: full hosting playing custom; Flags: fixed
|
||||
Name: "generator"; Description: "Generator"; Types: full hosting
|
||||
Name: "generator/sm"; Description: "Super Metroid ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 3145728; Flags: disablenouninstallwarning
|
||||
Name: "generator/dkc3"; Description: "Donkey Kong Country 3 ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 3145728; Flags: disablenouninstallwarning
|
||||
Name: "generator/smw"; Description: "Super Mario World ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 3145728; Flags: disablenouninstallwarning
|
||||
Name: "generator/soe"; Description: "Secret of Evermore ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 3145728; Flags: disablenouninstallwarning
|
||||
Name: "generator/l2ac"; Description: "Lufia II Ancient Cave ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 2621440; Flags: disablenouninstallwarning
|
||||
Name: "generator/lttp"; Description: "A Link to the Past ROM Setup and Enemizer"; Types: full hosting; ExtraDiskSpaceRequired: 5191680
|
||||
Name: "generator/oot"; Description: "Ocarina of Time ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 100663296; Flags: disablenouninstallwarning
|
||||
Name: "generator/zl"; Description: "Zillion ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 150000; Flags: disablenouninstallwarning
|
||||
Name: "generator/pkmn_r"; Description: "Pokemon Red ROM Setup"; Types: full hosting
|
||||
Name: "generator/pkmn_b"; Description: "Pokemon Blue ROM Setup"; Types: full hosting
|
||||
Name: "generator/mmbn3"; Description: "MegaMan Battle Network 3"; Types: full hosting; ExtraDiskSpaceRequired: 8388608; Flags: disablenouninstallwarning
|
||||
Name: "generator/ladx"; Description: "Link's Awakening DX ROM Setup"; Types: full hosting
|
||||
Name: "generator/tloz"; Description: "The Legend of Zelda ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 135168; Flags: disablenouninstallwarning
|
||||
Name: "server"; Description: "Server"; Types: full hosting
|
||||
Name: "client"; Description: "Clients"; Types: full playing
|
||||
Name: "client/sni"; Description: "SNI Client"; Types: full playing
|
||||
Name: "client/sni/lttp"; Description: "SNI Client - A Link to the Past Patch Setup"; Types: full playing; Flags: disablenouninstallwarning
|
||||
Name: "client/sni/sm"; Description: "SNI Client - Super Metroid Patch Setup"; Types: full playing; Flags: disablenouninstallwarning
|
||||
Name: "client/sni/dkc3"; Description: "SNI Client - Donkey Kong Country 3 Patch Setup"; Types: full playing; Flags: disablenouninstallwarning
|
||||
Name: "client/sni/smw"; Description: "SNI Client - Super Mario World Patch Setup"; Types: full playing; Flags: disablenouninstallwarning
|
||||
Name: "client/sni/l2ac"; Description: "SNI Client - Lufia II Ancient Cave Patch Setup"; Types: full playing; Flags: disablenouninstallwarning
|
||||
Name: "client/bizhawk"; Description: "BizHawk Client"; Types: full playing
|
||||
Name: "client/factorio"; Description: "Factorio"; Types: full playing
|
||||
Name: "client/kh2"; Description: "Kingdom Hearts 2"; Types: full playing
|
||||
Name: "client/minecraft"; Description: "Minecraft"; Types: full playing; ExtraDiskSpaceRequired: 226894278
|
||||
Name: "client/oot"; Description: "Ocarina of Time"; Types: full playing
|
||||
Name: "client/ff1"; Description: "Final Fantasy 1"; Types: full playing
|
||||
Name: "client/pkmn"; Description: "Pokemon Client"
|
||||
Name: "client/pkmn/red"; Description: "Pokemon Client - Pokemon Red Setup"; Types: full playing; ExtraDiskSpaceRequired: 1048576
|
||||
Name: "client/pkmn/blue"; Description: "Pokemon Client - Pokemon Blue Setup"; Types: full playing; ExtraDiskSpaceRequired: 1048576
|
||||
Name: "client/mmbn3"; Description: "MegaMan Battle Network 3 Client"; Types: full playing;
|
||||
Name: "client/ladx"; Description: "Link's Awakening Client"; Types: full playing; ExtraDiskSpaceRequired: 1048576
|
||||
Name: "client/cf"; Description: "ChecksFinder"; Types: full playing
|
||||
Name: "client/sc2"; Description: "Starcraft 2"; Types: full playing
|
||||
Name: "client/wargroove"; Description: "Wargroove"; Types: full playing
|
||||
Name: "client/zl"; Description: "Zillion"; Types: full playing
|
||||
Name: "client/tloz"; Description: "The Legend of Zelda"; Types: full playing
|
||||
Name: "client/advn"; Description: "Adventure"; Types: full playing
|
||||
Name: "client/ut"; Description: "Undertale"; Types: full playing
|
||||
Name: "client/text"; Description: "Text, to !command and chat"; Types: full playing
|
||||
Name: "core"; Description: "Archipelago"; Types: full minimal custom; Flags: fixed
|
||||
Name: "lttp_sprites"; Description: "Download ""A Link to the Past"" player sprites"; Types: full;
|
||||
|
||||
[Dirs]
|
||||
NAME: "{app}"; Flags: setntfscompression; Permissions: everyone-modify users-modify authusers-modify;
|
||||
|
||||
[Files]
|
||||
Source: "{code:GetROMPath}"; DestDir: "{app}"; DestName: "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc"; Flags: external; Components: client/sni/lttp or generator/lttp
|
||||
Source: "{code:GetSMROMPath}"; DestDir: "{app}"; DestName: "Super Metroid (JU).sfc"; Flags: external; Components: client/sni/sm or generator/sm
|
||||
Source: "{code:GetDKC3ROMPath}"; DestDir: "{app}"; DestName: "Donkey Kong Country 3 - Dixie Kong's Double Trouble! (USA) (En,Fr).sfc"; Flags: external; Components: client/sni/dkc3 or generator/dkc3
|
||||
Source: "{code:GetSMWROMPath}"; DestDir: "{app}"; DestName: "Super Mario World (USA).sfc"; Flags: external; Components: client/sni/smw or generator/smw
|
||||
Source: "{code:GetSoEROMPath}"; DestDir: "{app}"; DestName: "Secret of Evermore (USA).sfc"; Flags: external; Components: generator/soe
|
||||
Source: "{code:GetL2ACROMPath}"; DestDir: "{app}"; DestName: "Lufia II - Rise of the Sinistrals (USA).sfc"; Flags: external; Components: generator/l2ac
|
||||
Source: "{code:GetOoTROMPath}"; DestDir: "{app}"; DestName: "The Legend of Zelda - Ocarina of Time.z64"; Flags: external; Components: client/oot or generator/oot
|
||||
Source: "{code:GetZlROMPath}"; DestDir: "{app}"; DestName: "Zillion (UE) [!].sms"; Flags: external; Components: client/zl or generator/zl
|
||||
Source: "{code:GetRedROMPath}"; DestDir: "{app}"; DestName: "Pokemon Red (UE) [S][!].gb"; Flags: external; Components: client/pkmn/red or generator/pkmn_r
|
||||
Source: "{code:GetBlueROMPath}"; DestDir: "{app}"; DestName: "Pokemon Blue (UE) [S][!].gb"; Flags: external; Components: client/pkmn/blue or generator/pkmn_b
|
||||
Source: "{code:GetBN3ROMPath}"; DestDir: "{app}"; DestName: "Mega Man Battle Network 3 - Blue Version (USA).gba"; Flags: external; Components: client/mmbn3
|
||||
Source: "{code:GetLADXROMPath}"; DestDir: "{app}"; DestName: "Legend of Zelda, The - Link's Awakening DX (USA, Europe) (SGB Enhanced).gbc"; Flags: external; Components: client/ladx or generator/ladx
|
||||
Source: "{code:GetTLoZROMPath}"; DestDir: "{app}"; DestName: "Legend of Zelda, The (U) (PRG0) [!].nes"; Flags: external; Components: client/tloz or generator/tloz
|
||||
Source: "{code:GetAdvnROMPath}"; DestDir: "{app}"; DestName: "ADVNTURE.BIN"; Flags: external; Components: client/advn
|
||||
Source: "{#source_path}\*"; Excludes: "*.sfc, *.log, data\sprites\alttpr, SNI, EnemizerCLI, Archipelago*.exe"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
|
||||
Source: "{#source_path}\SNI\*"; Excludes: "*.sfc, *.log"; DestDir: "{app}\SNI"; Flags: ignoreversion recursesubdirs createallsubdirs; Components: client/sni
|
||||
Source: "{#source_path}\EnemizerCLI\*"; Excludes: "*.sfc, *.log"; DestDir: "{app}\EnemizerCLI"; Flags: ignoreversion recursesubdirs createallsubdirs; Components: generator/lttp
|
||||
|
||||
Source: "{#source_path}\ArchipelagoLauncher.exe"; DestDir: "{app}"; Flags: ignoreversion;
|
||||
Source: "{#source_path}\ArchipelagoLauncher(DEBUG).exe"; DestDir: "{app}"; Flags: ignoreversion;
|
||||
Source: "{#source_path}\ArchipelagoGenerate.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: generator
|
||||
Source: "{#source_path}\ArchipelagoServer.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: server
|
||||
Source: "{#source_path}\ArchipelagoFactorioClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/factorio
|
||||
Source: "{#source_path}\ArchipelagoTextClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/text
|
||||
Source: "{#source_path}\ArchipelagoSNIClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/sni
|
||||
Source: "{#source_path}\ArchipelagoBizHawkClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/bizhawk
|
||||
Source: "{#source_path}\ArchipelagoLinksAwakeningClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/ladx
|
||||
Source: "{#source_path}\ArchipelagoLttPAdjuster.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/sni/lttp or generator/lttp
|
||||
Source: "{#source_path}\ArchipelagoMinecraftClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/minecraft
|
||||
Source: "{#source_path}\ArchipelagoOoTClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/oot
|
||||
Source: "{#source_path}\ArchipelagoOoTAdjuster.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/oot
|
||||
Source: "{#source_path}\ArchipelagoZillionClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/zl
|
||||
Source: "{#source_path}\ArchipelagoFF1Client.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/ff1
|
||||
Source: "{#source_path}\ArchipelagoPokemonClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/pkmn
|
||||
Source: "{#source_path}\ArchipelagoChecksFinderClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/cf
|
||||
Source: "{#source_path}\ArchipelagoStarcraft2Client.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/sc2
|
||||
Source: "{#source_path}\ArchipelagoMMBN3Client.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/mmbn3
|
||||
Source: "{#source_path}\ArchipelagoZelda1Client.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/tloz
|
||||
Source: "{#source_path}\ArchipelagoWargrooveClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/wargroove
|
||||
Source: "{#source_path}\ArchipelagoKH2Client.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/kh2
|
||||
Source: "{#source_path}\ArchipelagoAdventureClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/advn
|
||||
Source: "{#source_path}\ArchipelagoUndertaleClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/ut
|
||||
Source: "{#source_path}\*"; Excludes: "*.sfc, *.log, data\sprites\alttpr, SNI, EnemizerCLI"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
|
||||
Source: "{#source_path}\SNI\*"; Excludes: "*.sfc, *.log"; DestDir: "{app}\SNI"; Flags: ignoreversion recursesubdirs createallsubdirs;
|
||||
Source: "{#source_path}\EnemizerCLI\*"; Excludes: "*.sfc, *.log"; DestDir: "{app}\EnemizerCLI"; Flags: ignoreversion recursesubdirs createallsubdirs;
|
||||
Source: "vc_redist.x64.exe"; DestDir: {tmp}; Flags: deleteafterinstall
|
||||
|
||||
[Icons]
|
||||
Name: "{group}\{#MyAppName} Folder"; Filename: "{app}";
|
||||
Name: "{group}\{#MyAppName} Launcher"; Filename: "{app}\ArchipelagoLauncher.exe"
|
||||
Name: "{group}\{#MyAppName} Server"; Filename: "{app}\ArchipelagoServer"; Components: server
|
||||
Name: "{group}\{#MyAppName} Text Client"; Filename: "{app}\ArchipelagoTextClient.exe"; Components: client/text
|
||||
Name: "{group}\{#MyAppName} SNI Client"; Filename: "{app}\ArchipelagoSNIClient.exe"; Components: client/sni
|
||||
Name: "{group}\{#MyAppName} BizHawk Client"; Filename: "{app}\ArchipelagoBizHawkClient.exe"; Components: client/bizhawk
|
||||
Name: "{group}\{#MyAppName} Factorio Client"; Filename: "{app}\ArchipelagoFactorioClient.exe"; Components: client/factorio
|
||||
Name: "{group}\{#MyAppName} Minecraft Client"; Filename: "{app}\ArchipelagoMinecraftClient.exe"; Components: client/minecraft
|
||||
Name: "{group}\{#MyAppName} Ocarina of Time Client"; Filename: "{app}\ArchipelagoOoTClient.exe"; Components: client/oot
|
||||
Name: "{group}\{#MyAppName} Zillion Client"; Filename: "{app}\ArchipelagoZillionClient.exe"; Components: client/zl
|
||||
Name: "{group}\{#MyAppName} Final Fantasy 1 Client"; Filename: "{app}\ArchipelagoFF1Client.exe"; Components: client/ff1
|
||||
Name: "{group}\{#MyAppName} Pokemon Client"; Filename: "{app}\ArchipelagoPokemonClient.exe"; Components: client/pkmn
|
||||
Name: "{group}\{#MyAppName} ChecksFinder Client"; Filename: "{app}\ArchipelagoChecksFinderClient.exe"; Components: client/cf
|
||||
Name: "{group}\{#MyAppName} Starcraft 2 Client"; Filename: "{app}\ArchipelagoStarcraft2Client.exe"; Components: client/sc2
|
||||
Name: "{group}\{#MyAppName} MegaMan Battle Network 3 Client"; Filename: "{app}\ArchipelagoMMBN3Client.exe"; Components: client/mmbn3
|
||||
Name: "{group}\{#MyAppName} The Legend of Zelda Client"; Filename: "{app}\ArchipelagoZelda1Client.exe"; Components: client/tloz
|
||||
Name: "{group}\{#MyAppName} Kingdom Hearts 2 Client"; Filename: "{app}\ArchipelagoKH2Client.exe"; Components: client/kh2
|
||||
Name: "{group}\{#MyAppName} Link's Awakening Client"; Filename: "{app}\ArchipelagoLinksAwakeningClient.exe"; Components: client/ladx
|
||||
Name: "{group}\{#MyAppName} Adventure Client"; Filename: "{app}\ArchipelagoAdventureClient.exe"; Components: client/advn
|
||||
Name: "{group}\{#MyAppName} Wargroove Client"; Filename: "{app}\ArchipelagoWargrooveClient.exe"; Components: client/wargroove
|
||||
Name: "{group}\{#MyAppName} Undertale Client"; Filename: "{app}\ArchipelagoUndertaleClient.exe"; Components: client/ut
|
||||
|
||||
Name: "{commondesktop}\{#MyAppName} Folder"; Filename: "{app}"; Tasks: desktopicon
|
||||
Name: "{commondesktop}\{#MyAppName} Launcher"; Filename: "{app}\ArchipelagoLauncher.exe"; Tasks: desktopicon
|
||||
Name: "{commondesktop}\{#MyAppName} Server"; Filename: "{app}\ArchipelagoServer"; Tasks: desktopicon; Components: server
|
||||
Name: "{commondesktop}\{#MyAppName} SNI Client"; Filename: "{app}\ArchipelagoSNIClient.exe"; Tasks: desktopicon; Components: client/sni
|
||||
Name: "{commondesktop}\{#MyAppName} BizHawk Client"; Filename: "{app}\ArchipelagoBizHawkClient.exe"; Tasks: desktopicon; Components: client/bizhawk
|
||||
Name: "{commondesktop}\{#MyAppName} Factorio Client"; Filename: "{app}\ArchipelagoFactorioClient.exe"; Tasks: desktopicon; Components: client/factorio
|
||||
Name: "{commondesktop}\{#MyAppName} Minecraft Client"; Filename: "{app}\ArchipelagoMinecraftClient.exe"; Tasks: desktopicon; Components: client/minecraft
|
||||
Name: "{commondesktop}\{#MyAppName} Ocarina of Time Client"; Filename: "{app}\ArchipelagoOoTClient.exe"; Tasks: desktopicon; Components: client/oot
|
||||
Name: "{commondesktop}\{#MyAppName} Zillion Client"; Filename: "{app}\ArchipelagoZillionClient.exe"; Tasks: desktopicon; Components: client/zl
|
||||
Name: "{commondesktop}\{#MyAppName} Final Fantasy 1 Client"; Filename: "{app}\ArchipelagoFF1Client.exe"; Tasks: desktopicon; Components: client/ff1
|
||||
Name: "{commondesktop}\{#MyAppName} Pokemon Client"; Filename: "{app}\ArchipelagoPokemonClient.exe"; Tasks: desktopicon; Components: client/pkmn
|
||||
Name: "{commondesktop}\{#MyAppName} ChecksFinder Client"; Filename: "{app}\ArchipelagoChecksFinderClient.exe"; Tasks: desktopicon; Components: client/cf
|
||||
Name: "{commondesktop}\{#MyAppName} Starcraft 2 Client"; Filename: "{app}\ArchipelagoStarcraft2Client.exe"; Tasks: desktopicon; Components: client/sc2
|
||||
Name: "{commondesktop}\{#MyAppName} MegaMan Battle Network 3 Client"; Filename: "{app}\ArchipelagoMMBN3Client.exe"; Tasks: desktopicon; Components: client/mmbn3
|
||||
Name: "{commondesktop}\{#MyAppName} The Legend of Zelda Client"; Filename: "{app}\ArchipelagoZelda1Client.exe"; Tasks: desktopicon; Components: client/tloz
|
||||
Name: "{commondesktop}\{#MyAppName} Wargroove Client"; Filename: "{app}\ArchipelagoWargrooveClient.exe"; Tasks: desktopicon; Components: client/wargroove
|
||||
Name: "{commondesktop}\{#MyAppName} Kingdom Hearts 2 Client"; Filename: "{app}\ArchipelagoKH2Client.exe"; Tasks: desktopicon; Components: client/kh2
|
||||
Name: "{commondesktop}\{#MyAppName} Link's Awakening Client"; Filename: "{app}\ArchipelagoLinksAwakeningClient.exe"; Tasks: desktopicon; Components: client/ladx
|
||||
Name: "{commondesktop}\{#MyAppName} Adventure Client"; Filename: "{app}\ArchipelagoAdventureClient.exe"; Tasks: desktopicon; Components: client/advn
|
||||
Name: "{commondesktop}\{#MyAppName} Undertale Client"; Filename: "{app}\ArchipelagoUndertaleClient.exe"; Tasks: desktopicon; Components: client/ut
|
||||
|
||||
[Run]
|
||||
|
||||
Filename: "{tmp}\vc_redist.x64.exe"; Parameters: "/passive /norestart"; Check: IsVCRedist64BitNeeded; StatusMsg: "Installing VC++ redistributable..."
|
||||
Filename: "{app}\ArchipelagoLttPAdjuster"; Parameters: "--update_sprites"; StatusMsg: "Updating Sprite Library..."; Components: client/sni/lttp or generator/lttp
|
||||
Filename: "{app}\ArchipelagoMinecraftClient.exe"; Parameters: "--install"; StatusMsg: "Installing Forge Server..."; Components: client/minecraft
|
||||
Filename: "{app}\ArchipelagoLttPAdjuster"; Parameters: "--update_sprites"; StatusMsg: "Updating Sprite Library..."; Flags: nowait; Components: lttp_sprites
|
||||
Filename: "{app}\ArchipelagoLauncher"; Parameters: "--update_settings"; StatusMsg: "Updating host.yaml..."; Flags: runasoriginaluser runhidden
|
||||
Filename: "{app}\ArchipelagoLauncher"; Description: "{cm:LaunchProgram,{#StringChange('Launcher', '&', '&&')}}"; Flags: nowait postinstall skipifsilent
|
||||
|
||||
@@ -206,101 +88,97 @@ Type: filesandordirs; Name: "{app}\EnemizerCLI*"
|
||||
|
||||
[Registry]
|
||||
|
||||
Root: HKCR; Subkey: ".aplttp"; ValueData: "{#MyAppName}patch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/sni
|
||||
Root: HKCR; Subkey: "{#MyAppName}patch"; ValueData: "Archipelago Binary Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/sni
|
||||
Root: HKCR; Subkey: "{#MyAppName}patch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: ""; Components: client/sni
|
||||
Root: HKCR; Subkey: "{#MyAppName}patch\shell\open\command"; ValueData: """{app}\ArchipelagoSNIClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/sni
|
||||
Root: HKCR; Subkey: ".aplttp"; ValueData: "{#MyAppName}patch"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}patch"; ValueData: "Archipelago Binary Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}patch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}patch\shell\open\command"; ValueData: """{app}\ArchipelagoSNIClient.exe"" ""%1"""; ValueType: string; ValueName: "";
|
||||
|
||||
Root: HKCR; Subkey: ".apsm"; ValueData: "{#MyAppName}smpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/sni
|
||||
Root: HKCR; Subkey: "{#MyAppName}smpatch"; ValueData: "Archipelago Super Metroid Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/sni
|
||||
Root: HKCR; Subkey: "{#MyAppName}smpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: ""; Components: client/sni
|
||||
Root: HKCR; Subkey: "{#MyAppName}smpatch\shell\open\command"; ValueData: """{app}\ArchipelagoSNIClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/sni
|
||||
Root: HKCR; Subkey: ".apsm"; ValueData: "{#MyAppName}smpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}smpatch"; ValueData: "Archipelago Super Metroid Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}smpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}smpatch\shell\open\command"; ValueData: """{app}\ArchipelagoSNIClient.exe"" ""%1"""; ValueType: string; ValueName: "";
|
||||
|
||||
Root: HKCR; Subkey: ".apdkc3"; ValueData: "{#MyAppName}dkc3patch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/sni
|
||||
Root: HKCR; Subkey: "{#MyAppName}dkc3patch"; ValueData: "Archipelago Donkey Kong Country 3 Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/sni
|
||||
Root: HKCR; Subkey: "{#MyAppName}dkc3patch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: ""; Components: client/sni
|
||||
Root: HKCR; Subkey: "{#MyAppName}dkc3patch\shell\open\command"; ValueData: """{app}\ArchipelagoSNIClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/sni
|
||||
Root: HKCR; Subkey: ".apdkc3"; ValueData: "{#MyAppName}dkc3patch"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}dkc3patch"; ValueData: "Archipelago Donkey Kong Country 3 Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}dkc3patch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}dkc3patch\shell\open\command"; ValueData: """{app}\ArchipelagoSNIClient.exe"" ""%1"""; ValueType: string; ValueName: "";
|
||||
|
||||
Root: HKCR; Subkey: ".apsmw"; ValueData: "{#MyAppName}smwpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/sni
|
||||
Root: HKCR; Subkey: "{#MyAppName}smwpatch"; ValueData: "Archipelago Super Mario World Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/sni
|
||||
Root: HKCR; Subkey: "{#MyAppName}smwpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: ""; Components: client/sni
|
||||
Root: HKCR; Subkey: "{#MyAppName}smwpatch\shell\open\command"; ValueData: """{app}\ArchipelagoSNIClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/sni
|
||||
Root: HKCR; Subkey: ".apsmw"; ValueData: "{#MyAppName}smwpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}smwpatch"; ValueData: "Archipelago Super Mario World Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}smwpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}smwpatch\shell\open\command"; ValueData: """{app}\ArchipelagoSNIClient.exe"" ""%1"""; ValueType: string; ValueName: "";
|
||||
|
||||
Root: HKCR; Subkey: ".apzl"; ValueData: "{#MyAppName}zlpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/zl
|
||||
Root: HKCR; Subkey: "{#MyAppName}zlpatch"; ValueData: "Archipelago Zillion Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/zl
|
||||
Root: HKCR; Subkey: "{#MyAppName}zlpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoZillionClient.exe,0"; ValueType: string; ValueName: ""; Components: client/zl
|
||||
Root: HKCR; Subkey: "{#MyAppName}zlpatch\shell\open\command"; ValueData: """{app}\ArchipelagoZillionClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/zl
|
||||
Root: HKCR; Subkey: ".apzl"; ValueData: "{#MyAppName}zlpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}zlpatch"; ValueData: "Archipelago Zillion Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}zlpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoZillionClient.exe,0"; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}zlpatch\shell\open\command"; ValueData: """{app}\ArchipelagoZillionClient.exe"" ""%1"""; ValueType: string; ValueName: "";
|
||||
|
||||
Root: HKCR; Subkey: ".apsmz3"; ValueData: "{#MyAppName}smz3patch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/sni
|
||||
Root: HKCR; Subkey: "{#MyAppName}smz3patch"; ValueData: "Archipelago SMZ3 Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/sni
|
||||
Root: HKCR; Subkey: "{#MyAppName}smz3patch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: ""; Components: client/sni
|
||||
Root: HKCR; Subkey: "{#MyAppName}smz3patch\shell\open\command"; ValueData: """{app}\ArchipelagoSNIClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/sni
|
||||
Root: HKCR; Subkey: ".apsmz3"; ValueData: "{#MyAppName}smz3patch"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}smz3patch"; ValueData: "Archipelago SMZ3 Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}smz3patch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}smz3patch\shell\open\command"; ValueData: """{app}\ArchipelagoSNIClient.exe"" ""%1"""; ValueType: string; ValueName: "";
|
||||
|
||||
Root: HKCR; Subkey: ".apsoe"; ValueData: "{#MyAppName}soepatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/sni
|
||||
Root: HKCR; Subkey: "{#MyAppName}soepatch"; ValueData: "Archipelago Secret of Evermore Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/sni
|
||||
Root: HKCR; Subkey: "{#MyAppName}soepatch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: ""; Components: client/sni
|
||||
Root: HKCR; Subkey: "{#MyAppName}soepatch\shell\open\command"; ValueData: """{app}\ArchipelagoSNIClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/sni
|
||||
Root: HKCR; Subkey: ".apsoe"; ValueData: "{#MyAppName}soepatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}soepatch"; ValueData: "Archipelago Secret of Evermore Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}soepatch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}soepatch\shell\open\command"; ValueData: """{app}\ArchipelagoSNIClient.exe"" ""%1"""; ValueType: string; ValueName: "";
|
||||
|
||||
Root: HKCR; Subkey: ".apl2ac"; ValueData: "{#MyAppName}l2acpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/sni
|
||||
Root: HKCR; Subkey: "{#MyAppName}l2acpatch"; ValueData: "Archipelago Lufia II Ancient Cave Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/sni
|
||||
Root: HKCR; Subkey: "{#MyAppName}l2acpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: ""; Components: client/sni
|
||||
Root: HKCR; Subkey: "{#MyAppName}l2acpatch\shell\open\command"; ValueData: """{app}\ArchipelagoSNIClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/sni
|
||||
Root: HKCR; Subkey: ".apl2ac"; ValueData: "{#MyAppName}l2acpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}l2acpatch"; ValueData: "Archipelago Lufia II Ancient Cave Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}l2acpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}l2acpatch\shell\open\command"; ValueData: """{app}\ArchipelagoSNIClient.exe"" ""%1"""; ValueType: string; ValueName: "";
|
||||
|
||||
Root: HKCR; Subkey: ".apmc"; ValueData: "{#MyAppName}mcdata"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/minecraft
|
||||
Root: HKCR; Subkey: "{#MyAppName}mcdata"; ValueData: "Archipelago Minecraft Data"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/minecraft
|
||||
Root: HKCR; Subkey: "{#MyAppName}mcdata\DefaultIcon"; ValueData: "{app}\ArchipelagoMinecraftClient.exe,0"; ValueType: string; ValueName: ""; Components: client/minecraft
|
||||
Root: HKCR; Subkey: "{#MyAppName}mcdata\shell\open\command"; ValueData: """{app}\ArchipelagoMinecraftClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/minecraft
|
||||
Root: HKCR; Subkey: ".apmc"; ValueData: "{#MyAppName}mcdata"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}mcdata"; ValueData: "Archipelago Minecraft Data"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}mcdata\DefaultIcon"; ValueData: "{app}\ArchipelagoMinecraftClient.exe,0"; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}mcdata\shell\open\command"; ValueData: """{app}\ArchipelagoMinecraftClient.exe"" ""%1"""; ValueType: string; ValueName: "";
|
||||
|
||||
Root: HKCR; Subkey: ".apz5"; ValueData: "{#MyAppName}n64zpf"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/oot
|
||||
Root: HKCR; Subkey: "{#MyAppName}n64zpf"; ValueData: "Archipelago Ocarina of Time Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/oot
|
||||
Root: HKCR; Subkey: "{#MyAppName}n64zpf\DefaultIcon"; ValueData: "{app}\ArchipelagoOoTClient.exe,0"; ValueType: string; ValueName: ""; Components: client/oot
|
||||
Root: HKCR; Subkey: "{#MyAppName}n64zpf\shell\open\command"; ValueData: """{app}\ArchipelagoOoTClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/oot
|
||||
Root: HKCR; Subkey: ".apz5"; ValueData: "{#MyAppName}n64zpf"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}n64zpf"; ValueData: "Archipelago Ocarina of Time Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}n64zpf\DefaultIcon"; ValueData: "{app}\ArchipelagoOoTClient.exe,0"; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}n64zpf\shell\open\command"; ValueData: """{app}\ArchipelagoOoTClient.exe"" ""%1"""; ValueType: string; ValueName: "";
|
||||
|
||||
Root: HKCR; Subkey: ".apred"; ValueData: "{#MyAppName}pkmnrpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/pkmn
|
||||
Root: HKCR; Subkey: "{#MyAppName}pkmnrpatch"; ValueData: "Archipelago Pokemon Red Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/pkmn
|
||||
Root: HKCR; Subkey: "{#MyAppName}pkmnrpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoPokemonClient.exe,0"; ValueType: string; ValueName: ""; Components: client/pkmn
|
||||
Root: HKCR; Subkey: "{#MyAppName}pkmnrpatch\shell\open\command"; ValueData: """{app}\ArchipelagoPokemonClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/pkmn
|
||||
Root: HKCR; Subkey: ".apred"; ValueData: "{#MyAppName}pkmnrpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}pkmnrpatch"; ValueData: "Archipelago Pokemon Red Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}pkmnrpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoPokemonClient.exe,0"; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}pkmnrpatch\shell\open\command"; ValueData: """{app}\ArchipelagoPokemonClient.exe"" ""%1"""; ValueType: string; ValueName: "";
|
||||
|
||||
Root: HKCR; Subkey: ".apblue"; ValueData: "{#MyAppName}pkmnbpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/pkmn
|
||||
Root: HKCR; Subkey: "{#MyAppName}pkmnbpatch"; ValueData: "Archipelago Pokemon Blue Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/pkmn
|
||||
Root: HKCR; Subkey: "{#MyAppName}pkmnbpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoPokemonClient.exe,0"; ValueType: string; ValueName: ""; Components: client/pkmn
|
||||
Root: HKCR; Subkey: "{#MyAppName}pkmnbpatch\shell\open\command"; ValueData: """{app}\ArchipelagoPokemonClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/pkmn
|
||||
Root: HKCR; Subkey: ".apblue"; ValueData: "{#MyAppName}pkmnbpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}pkmnbpatch"; ValueData: "Archipelago Pokemon Blue Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}pkmnbpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoPokemonClient.exe,0"; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}pkmnbpatch\shell\open\command"; ValueData: """{app}\ArchipelagoPokemonClient.exe"" ""%1"""; ValueType: string; ValueName: "";
|
||||
|
||||
Root: HKCR; Subkey: ".apbn3"; ValueData: "{#MyAppName}bn3bpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/mmbn3
|
||||
Root: HKCR; Subkey: "{#MyAppName}bn3bpatch"; ValueData: "Archipelago MegaMan Battle Network 3 Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/mmbn3
|
||||
Root: HKCR; Subkey: "{#MyAppName}bn3bpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoMMBN3Client.exe,0"; ValueType: string; ValueName: ""; Components: client/mmbn3
|
||||
Root: HKCR; Subkey: "{#MyAppName}bn3bpatch\shell\open\command"; ValueData: """{app}\ArchipelagoMMBN3Client.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/mmbn3
|
||||
Root: HKCR; Subkey: ".apbn3"; ValueData: "{#MyAppName}bn3bpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}bn3bpatch"; ValueData: "Archipelago MegaMan Battle Network 3 Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}bn3bpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoMMBN3Client.exe,0"; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}bn3bpatch\shell\open\command"; ValueData: """{app}\ArchipelagoMMBN3Client.exe"" ""%1"""; ValueType: string; ValueName: "";
|
||||
|
||||
Root: HKCR; Subkey: ".apladx"; ValueData: "{#MyAppName}ladxpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/ladx
|
||||
Root: HKCR; Subkey: "{#MyAppName}ladxpatch"; ValueData: "Archipelago Links Awakening DX Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/ladx
|
||||
Root: HKCR; Subkey: "{#MyAppName}ladxpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoLinksAwakeningClient.exe,0"; ValueType: string; ValueName: ""; Components: client/ladx
|
||||
Root: HKCR; Subkey: "{#MyAppName}ladxpatch\shell\open\command"; ValueData: """{app}\ArchipelagoLinksAwakeningClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/ladx
|
||||
Root: HKCR; Subkey: ".apladx"; ValueData: "{#MyAppName}ladxpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}ladxpatch"; ValueData: "Archipelago Links Awakening DX Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}ladxpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoLinksAwakeningClient.exe,0"; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}ladxpatch\shell\open\command"; ValueData: """{app}\ArchipelagoLinksAwakeningClient.exe"" ""%1"""; ValueType: string; ValueName: "";
|
||||
|
||||
Root: HKCR; Subkey: ".aptloz"; ValueData: "{#MyAppName}tlozpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/tloz
|
||||
Root: HKCR; Subkey: "{#MyAppName}tlozpatch"; ValueData: "Archipelago The Legend of Zelda Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/tloz
|
||||
Root: HKCR; Subkey: "{#MyAppName}tlozpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoZelda1Client.exe,0"; ValueType: string; ValueName: ""; Components: client/tloz
|
||||
Root: HKCR; Subkey: "{#MyAppName}tlozpatch\shell\open\command"; ValueData: """{app}\ArchipelagoZelda1Client.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/tloz
|
||||
Root: HKCR; Subkey: ".aptloz"; ValueData: "{#MyAppName}tlozpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}tlozpatch"; ValueData: "Archipelago The Legend of Zelda Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}tlozpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoZelda1Client.exe,0"; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}tlozpatch\shell\open\command"; ValueData: """{app}\ArchipelagoZelda1Client.exe"" ""%1"""; ValueType: string; ValueName: "";
|
||||
|
||||
Root: HKCR; Subkey: ".apadvn"; ValueData: "{#MyAppName}advnpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/advn
|
||||
Root: HKCR; Subkey: "{#MyAppName}advnpatch"; ValueData: "Archipelago Adventure Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/advn
|
||||
Root: HKCR; Subkey: "{#MyAppName}advnpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoAdventureClient.exe,0"; ValueType: string; ValueName: ""; Components: client/advn
|
||||
Root: HKCR; Subkey: "{#MyAppName}advnpatch\shell\open\command"; ValueData: """{app}\ArchipelagoAdventureClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/advn
|
||||
Root: HKCR; Subkey: ".apadvn"; ValueData: "{#MyAppName}advnpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}advnpatch"; ValueData: "Archipelago Adventure Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}advnpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoAdventureClient.exe,0"; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}advnpatch\shell\open\command"; ValueData: """{app}\ArchipelagoAdventureClient.exe"" ""%1"""; ValueType: string; ValueName: "";
|
||||
|
||||
Root: HKCR; Subkey: ".archipelago"; ValueData: "{#MyAppName}multidata"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: server
|
||||
Root: HKCR; Subkey: "{#MyAppName}multidata"; ValueData: "Archipelago Server Data"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: server
|
||||
Root: HKCR; Subkey: "{#MyAppName}multidata\DefaultIcon"; ValueData: "{app}\ArchipelagoServer.exe,0"; ValueType: string; ValueName: ""; Components: server
|
||||
Root: HKCR; Subkey: "{#MyAppName}multidata\shell\open\command"; ValueData: """{app}\ArchipelagoServer.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: server
|
||||
Root: HKCR; Subkey: ".archipelago"; ValueData: "{#MyAppName}multidata"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}multidata"; ValueData: "Archipelago Server Data"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}multidata\DefaultIcon"; ValueData: "{app}\ArchipelagoServer.exe,0"; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}multidata\shell\open\command"; ValueData: """{app}\ArchipelagoServer.exe"" ""%1"""; ValueType: string; ValueName: "";
|
||||
|
||||
Root: HKCR; Subkey: "archipelago"; ValueType: "string"; ValueData: "Archipegalo Protocol"; Flags: uninsdeletekey; Components: client/text
|
||||
Root: HKCR; Subkey: "archipelago"; ValueType: "string"; ValueName: "URL Protocol"; ValueData: ""; Components: client/text
|
||||
Root: HKCR; Subkey: "archipelago\DefaultIcon"; ValueType: "string"; ValueData: "{app}\ArchipelagoTextClient.exe,0"; Components: client/text
|
||||
Root: HKCR; Subkey: "archipelago\shell\open\command"; ValueType: "string"; ValueData: """{app}\ArchipelagoTextClient.exe"" ""%1"""; Components: client/text
|
||||
Root: HKCR; Subkey: "archipelago"; ValueType: "string"; ValueData: "Archipegalo Protocol"; Flags: uninsdeletekey;
|
||||
Root: HKCR; Subkey: "archipelago"; ValueType: "string"; ValueName: "URL Protocol"; ValueData: "";
|
||||
Root: HKCR; Subkey: "archipelago\DefaultIcon"; ValueType: "string"; ValueData: "{app}\ArchipelagoTextClient.exe,0";
|
||||
Root: HKCR; Subkey: "archipelago\shell\open\command"; ValueType: "string"; ValueData: """{app}\ArchipelagoTextClient.exe"" ""%1""";
|
||||
|
||||
[Code]
|
||||
const
|
||||
SHCONTCH_NOPROGRESSBOX = 4;
|
||||
SHCONTCH_RESPONDYESTOALL = 16;
|
||||
|
||||
// See: https://stackoverflow.com/a/51614652/2287576
|
||||
function IsVCRedist64BitNeeded(): boolean;
|
||||
var
|
||||
@@ -320,594 +198,3 @@ begin
|
||||
Result := True;
|
||||
end;
|
||||
end;
|
||||
|
||||
var R : longint;
|
||||
|
||||
var lttprom: string;
|
||||
var LttPROMFilePage: TInputFileWizardPage;
|
||||
|
||||
var smrom: string;
|
||||
var SMRomFilePage: TInputFileWizardPage;
|
||||
|
||||
var dkc3rom: string;
|
||||
var DKC3RomFilePage: TInputFileWizardPage;
|
||||
|
||||
var smwrom: string;
|
||||
var SMWRomFilePage: TInputFileWizardPage;
|
||||
|
||||
var soerom: string;
|
||||
var SoERomFilePage: TInputFileWizardPage;
|
||||
|
||||
var l2acrom: string;
|
||||
var L2ACROMFilePage: TInputFileWizardPage;
|
||||
|
||||
var ootrom: string;
|
||||
var OoTROMFilePage: TInputFileWizardPage;
|
||||
|
||||
var zlrom: string;
|
||||
var ZlROMFilePage: TInputFileWizardPage;
|
||||
|
||||
var redrom: string;
|
||||
var RedROMFilePage: TInputFileWizardPage;
|
||||
|
||||
var bluerom: string;
|
||||
var BlueROMFilePage: TInputFileWizardPage;
|
||||
|
||||
var bn3rom: string;
|
||||
var BN3ROMFilePage: TInputFileWizardPage;
|
||||
|
||||
var ladxrom: string;
|
||||
var LADXROMFilePage: TInputFileWizardPage;
|
||||
|
||||
var tlozrom: string;
|
||||
var TLoZROMFilePage: TInputFileWizardPage;
|
||||
|
||||
var advnrom: string;
|
||||
var AdvnROMFilePage: TInputFileWizardPage;
|
||||
|
||||
function GetSNESMD5OfFile(const rom: string): string;
|
||||
var data: AnsiString;
|
||||
begin
|
||||
if LoadStringFromFile(rom, data) then
|
||||
begin
|
||||
if Length(data) mod 1024 = 512 then
|
||||
begin
|
||||
data := copy(data, 513, Length(data)-512);
|
||||
end;
|
||||
Result := GetMD5OfString(data);
|
||||
end;
|
||||
end;
|
||||
|
||||
function GetSMSMD5OfFile(const rom: string): string;
|
||||
var data: AnsiString;
|
||||
begin
|
||||
if LoadStringFromFile(rom, data) then
|
||||
begin
|
||||
Result := GetMD5OfString(data);
|
||||
end;
|
||||
end;
|
||||
|
||||
function CheckRom(name: string; hash: string): string;
|
||||
var rom: string;
|
||||
begin
|
||||
log('Handling ' + name)
|
||||
rom := FileSearch(name, WizardDirValue());
|
||||
if Length(rom) > 0 then
|
||||
begin
|
||||
log('existing ROM found');
|
||||
log(IntToStr(CompareStr(GetSNESMD5OfFile(rom), hash)));
|
||||
if CompareStr(GetSNESMD5OfFile(rom), hash) = 0 then
|
||||
begin
|
||||
log('existing ROM verified');
|
||||
Result := rom;
|
||||
exit;
|
||||
end;
|
||||
log('existing ROM failed verification');
|
||||
end;
|
||||
end;
|
||||
|
||||
function CheckSMSRom(name: string; hash: string): string;
|
||||
var rom: string;
|
||||
begin
|
||||
log('Handling ' + name)
|
||||
rom := FileSearch(name, WizardDirValue());
|
||||
if Length(rom) > 0 then
|
||||
begin
|
||||
log('existing ROM found');
|
||||
log(IntToStr(CompareStr(GetSMSMD5OfFile(rom), hash)));
|
||||
if CompareStr(GetSMSMD5OfFile(rom), hash) = 0 then
|
||||
begin
|
||||
log('existing ROM verified');
|
||||
Result := rom;
|
||||
exit;
|
||||
end;
|
||||
log('existing ROM failed verification');
|
||||
end;
|
||||
end;
|
||||
|
||||
function CheckNESRom(name: string; hash: string): string;
|
||||
var rom: string;
|
||||
begin
|
||||
log('Handling ' + name)
|
||||
rom := FileSearch(name, WizardDirValue());
|
||||
if Length(rom) > 0 then
|
||||
begin
|
||||
log('existing ROM found');
|
||||
log(IntToStr(CompareStr(GetSMSMD5OfFile(rom), hash)));
|
||||
if CompareStr(GetSMSMD5OfFile(rom), hash) = 0 then
|
||||
begin
|
||||
log('existing ROM verified');
|
||||
Result := rom;
|
||||
exit;
|
||||
end;
|
||||
log('existing ROM failed verification');
|
||||
end;
|
||||
end;
|
||||
|
||||
function AddRomPage(name: string): TInputFileWizardPage;
|
||||
begin
|
||||
Result :=
|
||||
CreateInputFilePage(
|
||||
wpSelectComponents,
|
||||
'Select ROM File',
|
||||
'Where is your ' + name + ' located?',
|
||||
'Select the file, then click Next.');
|
||||
|
||||
Result.Add(
|
||||
'Location of ROM file:',
|
||||
'SNES ROM files|*.sfc;*.smc|All files|*.*',
|
||||
'.sfc');
|
||||
end;
|
||||
|
||||
|
||||
function AddGBRomPage(name: string): TInputFileWizardPage;
|
||||
begin
|
||||
Result :=
|
||||
CreateInputFilePage(
|
||||
wpSelectComponents,
|
||||
'Select ROM File',
|
||||
'Where is your ' + name + ' located?',
|
||||
'Select the file, then click Next.');
|
||||
|
||||
Result.Add(
|
||||
'Location of ROM file:',
|
||||
'GB ROM files|*.gb;*.gbc|All files|*.*',
|
||||
'.gb');
|
||||
end;
|
||||
|
||||
function AddGBARomPage(name: string): TInputFileWizardPage;
|
||||
begin
|
||||
Result :=
|
||||
CreateInputFilePage(
|
||||
wpSelectComponents,
|
||||
'Select ROM File',
|
||||
'Where is your ' + name + ' located?',
|
||||
'Select the file, then click Next.');
|
||||
Result.Add(
|
||||
'Location of ROM file:',
|
||||
'GBA ROM files|*.gba|All files|*.*',
|
||||
'.gba');
|
||||
end;
|
||||
|
||||
function AddSMSRomPage(name: string): TInputFileWizardPage;
|
||||
begin
|
||||
Result :=
|
||||
CreateInputFilePage(
|
||||
wpSelectComponents,
|
||||
'Select ROM File',
|
||||
'Where is your ' + name + ' located?',
|
||||
'Select the file, then click Next.');
|
||||
Result.Add(
|
||||
'Location of ROM file:',
|
||||
'SMS ROM files|*.sms|All files|*.*',
|
||||
'.sms');
|
||||
end;
|
||||
|
||||
function AddNESRomPage(name: string): TInputFileWizardPage;
|
||||
begin
|
||||
Result :=
|
||||
CreateInputFilePage(
|
||||
wpSelectComponents,
|
||||
'Select ROM File',
|
||||
'Where is your ' + name + ' located?',
|
||||
'Select the file, then click Next.');
|
||||
|
||||
Result.Add(
|
||||
'Location of ROM file:',
|
||||
'NES ROM files|*.nes|All files|*.*',
|
||||
'.nes');
|
||||
end;
|
||||
|
||||
procedure AddOoTRomPage();
|
||||
begin
|
||||
ootrom := FileSearch('The Legend of Zelda - Ocarina of Time.z64', WizardDirValue());
|
||||
if Length(ootrom) > 0 then
|
||||
begin
|
||||
log('existing ROM found');
|
||||
log(IntToStr(CompareStr(GetMD5OfFile(ootrom), '5bd1fe107bf8106b2ab6650abecd54d6'))); // normal
|
||||
log(IntToStr(CompareStr(GetMD5OfFile(ootrom), '6697768a7a7df2dd27a692a2638ea90b'))); // byteswapped
|
||||
log(IntToStr(CompareStr(GetMD5OfFile(ootrom), '05f0f3ebacbc8df9243b6148ffe4792f'))); // decompressed
|
||||
if (CompareStr(GetMD5OfFile(ootrom), '5bd1fe107bf8106b2ab6650abecd54d6') = 0) or (CompareStr(GetMD5OfFile(ootrom), '6697768a7a7df2dd27a692a2638ea90b') = 0) or (CompareStr(GetMD5OfFile(ootrom), '05f0f3ebacbc8df9243b6148ffe4792f') = 0) then
|
||||
begin
|
||||
log('existing ROM verified');
|
||||
exit;
|
||||
end;
|
||||
log('existing ROM failed verification');
|
||||
end;
|
||||
ootrom := ''
|
||||
OoTROMFilePage :=
|
||||
CreateInputFilePage(
|
||||
wpSelectComponents,
|
||||
'Select ROM File',
|
||||
'Where is your OoT 1.0 ROM located?',
|
||||
'Select the file, then click Next.');
|
||||
|
||||
OoTROMFilePage.Add(
|
||||
'Location of ROM file:',
|
||||
'N64 ROM files (*.z64, *.n64)|*.z64;*.n64|All files|*.*',
|
||||
'.z64');
|
||||
end;
|
||||
|
||||
function AddA26Page(name: string): TInputFileWizardPage;
|
||||
begin
|
||||
Result :=
|
||||
CreateInputFilePage(
|
||||
wpSelectComponents,
|
||||
'Select ROM File',
|
||||
'Where is your ' + name + ' located?',
|
||||
'Select the file, then click Next.');
|
||||
|
||||
Result.Add(
|
||||
'Location of ROM file:',
|
||||
'A2600 ROM files|*.BIN;*.a26|All files|*.*',
|
||||
'.BIN');
|
||||
end;
|
||||
|
||||
function NextButtonClick(CurPageID: Integer): Boolean;
|
||||
begin
|
||||
if (assigned(LttPROMFilePage)) and (CurPageID = LttPROMFilePage.ID) then
|
||||
Result := not (LttPROMFilePage.Values[0] = '')
|
||||
else if (assigned(SMROMFilePage)) and (CurPageID = SMROMFilePage.ID) then
|
||||
Result := not (SMROMFilePage.Values[0] = '')
|
||||
else if (assigned(DKC3ROMFilePage)) and (CurPageID = DKC3ROMFilePage.ID) then
|
||||
Result := not (DKC3ROMFilePage.Values[0] = '')
|
||||
else if (assigned(SMWROMFilePage)) and (CurPageID = SMWROMFilePage.ID) then
|
||||
Result := not (SMWROMFilePage.Values[0] = '')
|
||||
else if (assigned(SoEROMFilePage)) and (CurPageID = SoEROMFilePage.ID) then
|
||||
Result := not (SoEROMFilePage.Values[0] = '')
|
||||
else if (assigned(L2ACROMFilePage)) and (CurPageID = L2ACROMFilePage.ID) then
|
||||
Result := not (L2ACROMFilePage.Values[0] = '')
|
||||
else if (assigned(OoTROMFilePage)) and (CurPageID = OoTROMFilePage.ID) then
|
||||
Result := not (OoTROMFilePage.Values[0] = '')
|
||||
else if (assigned(BN3ROMFilePage)) and (CurPageID = BN3ROMFilePage.ID) then
|
||||
Result := not (BN3ROMFilePage.Values[0] = '')
|
||||
else if (assigned(ZlROMFilePage)) and (CurPageID = ZlROMFilePage.ID) then
|
||||
Result := not (ZlROMFilePage.Values[0] = '')
|
||||
else if (assigned(RedROMFilePage)) and (CurPageID = RedROMFilePage.ID) then
|
||||
Result := not (RedROMFilePage.Values[0] = '')
|
||||
else if (assigned(BlueROMFilePage)) and (CurPageID = BlueROMFilePage.ID) then
|
||||
Result := not (BlueROMFilePage.Values[0] = '')
|
||||
else if (assigned(LADXROMFilePage)) and (CurPageID = LADXROMFilePage.ID) then
|
||||
Result := not (LADXROMFilePage.Values[0] = '')
|
||||
else if (assigned(TLoZROMFilePage)) and (CurPageID = TLoZROMFilePage.ID) then
|
||||
Result := not (TLoZROMFilePage.Values[0] = '')
|
||||
else if (assigned(AdvnROMFilePage)) and (CurPageID = AdvnROMFilePage.ID) then
|
||||
Result := not (AdvnROMFilePage.Values[0] = '')
|
||||
else
|
||||
Result := True;
|
||||
end;
|
||||
|
||||
function GetROMPath(Param: string): string;
|
||||
begin
|
||||
if Length(lttprom) > 0 then
|
||||
Result := lttprom
|
||||
else if Assigned(LttPRomFilePage) then
|
||||
begin
|
||||
R := CompareStr(GetSNESMD5OfFile(LttPROMFilePage.Values[0]), '03a63945398191337e896e5771f77173')
|
||||
if R <> 0 then
|
||||
MsgBox('ALttP ROM validation failed. Very likely wrong file.', mbInformation, MB_OK);
|
||||
|
||||
Result := LttPROMFilePage.Values[0]
|
||||
end
|
||||
else
|
||||
Result := '';
|
||||
end;
|
||||
|
||||
function GetSMROMPath(Param: string): string;
|
||||
begin
|
||||
if Length(smrom) > 0 then
|
||||
Result := smrom
|
||||
else if Assigned(SMRomFilePage) then
|
||||
begin
|
||||
R := CompareStr(GetSNESMD5OfFile(SMROMFilePage.Values[0]), '21f3e98df4780ee1c667b84e57d88675')
|
||||
if R <> 0 then
|
||||
MsgBox('Super Metroid ROM validation failed. Very likely wrong file.', mbInformation, MB_OK);
|
||||
|
||||
Result := SMROMFilePage.Values[0]
|
||||
end
|
||||
else
|
||||
Result := '';
|
||||
end;
|
||||
|
||||
function GetDKC3ROMPath(Param: string): string;
|
||||
begin
|
||||
if Length(dkc3rom) > 0 then
|
||||
Result := dkc3rom
|
||||
else if Assigned(DKC3RomFilePage) then
|
||||
begin
|
||||
R := CompareStr(GetSNESMD5OfFile(DKC3ROMFilePage.Values[0]), '120abf304f0c40fe059f6a192ed4f947')
|
||||
if R <> 0 then
|
||||
MsgBox('Donkey Kong Country 3 ROM validation failed. Very likely wrong file.', mbInformation, MB_OK);
|
||||
|
||||
Result := DKC3ROMFilePage.Values[0]
|
||||
end
|
||||
else
|
||||
Result := '';
|
||||
end;
|
||||
|
||||
function GetSMWROMPath(Param: string): string;
|
||||
begin
|
||||
if Length(smwrom) > 0 then
|
||||
Result := smwrom
|
||||
else if Assigned(SMWRomFilePage) then
|
||||
begin
|
||||
R := CompareStr(GetSNESMD5OfFile(SMWROMFilePage.Values[0]), 'cdd3c8c37322978ca8669b34bc89c804')
|
||||
if R <> 0 then
|
||||
MsgBox('Super Mario World ROM validation failed. Very likely wrong file.', mbInformation, MB_OK);
|
||||
|
||||
Result := SMWROMFilePage.Values[0]
|
||||
end
|
||||
else
|
||||
Result := '';
|
||||
end;
|
||||
|
||||
function GetSoEROMPath(Param: string): string;
|
||||
begin
|
||||
if Length(soerom) > 0 then
|
||||
Result := soerom
|
||||
else if Assigned(SoERomFilePage) then
|
||||
begin
|
||||
R := CompareStr(GetSNESMD5OfFile(SoEROMFilePage.Values[0]), '6e9c94511d04fac6e0a1e582c170be3a')
|
||||
if R <> 0 then
|
||||
MsgBox('Secret of Evermore ROM validation failed. Very likely wrong file.', mbInformation, MB_OK);
|
||||
|
||||
Result := SoEROMFilePage.Values[0]
|
||||
end
|
||||
else
|
||||
Result := '';
|
||||
end;
|
||||
|
||||
function GetOoTROMPath(Param: string): string;
|
||||
begin
|
||||
if Length(ootrom) > 0 then
|
||||
Result := ootrom
|
||||
else if Assigned(OoTROMFilePage) then
|
||||
begin
|
||||
R := CompareStr(GetMD5OfFile(OoTROMFilePage.Values[0]), '5bd1fe107bf8106b2ab6650abecd54d6') * CompareStr(GetMD5OfFile(OoTROMFilePage.Values[0]), '6697768a7a7df2dd27a692a2638ea90b') * CompareStr(GetMD5OfFile(OoTROMFilePage.Values[0]), '05f0f3ebacbc8df9243b6148ffe4792f');
|
||||
if R <> 0 then
|
||||
MsgBox('OoT ROM validation failed. Very likely wrong file.', mbInformation, MB_OK);
|
||||
|
||||
Result := OoTROMFilePage.Values[0]
|
||||
end
|
||||
else
|
||||
Result := '';
|
||||
end;
|
||||
|
||||
function GetL2ACROMPath(Param: string): string;
|
||||
begin
|
||||
if Length(l2acrom) > 0 then
|
||||
Result := l2acrom
|
||||
else if Assigned(L2ACROMFilePage) then
|
||||
begin
|
||||
R := CompareStr(GetSNESMD5OfFile(L2ACROMFilePage.Values[0]), '6efc477d6203ed2b3b9133c1cd9e9c5d')
|
||||
if R <> 0 then
|
||||
MsgBox('Lufia II ROM validation failed. Very likely wrong file.', mbInformation, MB_OK);
|
||||
|
||||
Result := L2ACROMFilePage.Values[0]
|
||||
end
|
||||
else
|
||||
Result := '';
|
||||
end;
|
||||
|
||||
function GetZlROMPath(Param: string): string;
|
||||
begin
|
||||
if Length(zlrom) > 0 then
|
||||
Result := zlrom
|
||||
else if Assigned(ZlROMFilePage) then
|
||||
begin
|
||||
R := CompareStr(GetMD5OfFile(ZlROMFilePage.Values[0]), 'd4bf9e7bcf9a48da53785d2ae7bc4270');
|
||||
if R <> 0 then
|
||||
MsgBox('Zillion ROM validation failed. Very likely wrong file.', mbInformation, MB_OK);
|
||||
|
||||
Result := ZlROMFilePage.Values[0]
|
||||
end
|
||||
else
|
||||
Result := '';
|
||||
end;
|
||||
|
||||
function GetRedROMPath(Param: string): string;
|
||||
begin
|
||||
if Length(redrom) > 0 then
|
||||
Result := redrom
|
||||
else if Assigned(RedROMFilePage) then
|
||||
begin
|
||||
R := CompareStr(GetMD5OfFile(RedROMFilePage.Values[0]), '3d45c1ee9abd5738df46d2bdda8b57dc')
|
||||
if R <> 0 then
|
||||
MsgBox('Pokemon Red ROM validation failed. Very likely wrong file.', mbInformation, MB_OK);
|
||||
|
||||
Result := RedROMFilePage.Values[0]
|
||||
end
|
||||
else
|
||||
Result := '';
|
||||
end;
|
||||
|
||||
function GetBlueROMPath(Param: string): string;
|
||||
begin
|
||||
if Length(bluerom) > 0 then
|
||||
Result := bluerom
|
||||
else if Assigned(BlueROMFilePage) then
|
||||
begin
|
||||
R := CompareStr(GetMD5OfFile(BlueROMFilePage.Values[0]), '50927e843568814f7ed45ec4f944bd8b')
|
||||
if R <> 0 then
|
||||
MsgBox('Pokemon Blue ROM validation failed. Very likely wrong file.', mbInformation, MB_OK);
|
||||
|
||||
Result := BlueROMFilePage.Values[0]
|
||||
end
|
||||
else
|
||||
Result := '';
|
||||
end;
|
||||
|
||||
function GetTLoZROMPath(Param: string): string;
|
||||
begin
|
||||
if Length(tlozrom) > 0 then
|
||||
Result := tlozrom
|
||||
else if Assigned(TLoZROMFilePage) then
|
||||
begin
|
||||
R := CompareStr(GetMD5OfFile(TLoZROMFilePage.Values[0]), '337bd6f1a1163df31bf2633665589ab0');
|
||||
if R <> 0 then
|
||||
MsgBox('The Legend of Zelda ROM validation failed. Very likely wrong file.', mbInformation, MB_OK);
|
||||
|
||||
Result := TLoZROMFilePage.Values[0]
|
||||
end
|
||||
else
|
||||
Result := '';
|
||||
end;
|
||||
|
||||
function GetLADXROMPath(Param: string): string;
|
||||
begin
|
||||
if Length(ladxrom) > 0 then
|
||||
Result := ladxrom
|
||||
else if Assigned(LADXROMFilePage) then
|
||||
begin
|
||||
R := CompareStr(GetMD5OfFile(LADXROMFilePage.Values[0]), '07c211479386825042efb4ad31bb525f')
|
||||
if R <> 0 then
|
||||
MsgBox('Link''s Awakening DX ROM validation failed. Very likely wrong file.', mbInformation, MB_OK);
|
||||
|
||||
Result := LADXROMFilePage.Values[0]
|
||||
end
|
||||
else
|
||||
Result := '';
|
||||
end;
|
||||
|
||||
function GetAdvnROMPath(Param: string): string;
|
||||
begin
|
||||
if Length(advnrom) > 0 then
|
||||
Result := advnrom
|
||||
else if Assigned(AdvnROMFilePage) then
|
||||
begin
|
||||
R := CompareStr(GetMD5OfFile(AdvnROMFilePage.Values[0]), '157bddb7192754a45372be196797f284');
|
||||
if R <> 0 then
|
||||
MsgBox('Adventure ROM validation failed. Very likely wrong file.', mbInformation, MB_OK);
|
||||
|
||||
Result := AdvnROMFilePage.Values[0]
|
||||
end
|
||||
else
|
||||
Result := '';
|
||||
end;
|
||||
|
||||
function GetBN3ROMPath(Param: string): string;
|
||||
begin
|
||||
if Length(bn3rom) > 0 then
|
||||
Result := bn3rom
|
||||
else if Assigned(BN3ROMFilePage) then
|
||||
begin
|
||||
R := CompareStr(GetMD5OfFile(BN3ROMFilePage.Values[0]), '6fe31df0144759b34ad666badaacc442')
|
||||
if R <> 0 then
|
||||
MsgBox('MegaMan Battle Network 3 Blue ROM validation failed. Very likely wrong file.', mbInformation, MB_OK);
|
||||
|
||||
Result := BN3ROMFilePage.Values[0]
|
||||
end
|
||||
else
|
||||
Result := '';
|
||||
end;
|
||||
|
||||
procedure InitializeWizard();
|
||||
begin
|
||||
AddOoTRomPage();
|
||||
|
||||
lttprom := CheckRom('Zelda no Densetsu - Kamigami no Triforce (Japan).sfc', '03a63945398191337e896e5771f77173');
|
||||
if Length(lttprom) = 0 then
|
||||
LttPROMFilePage:= AddRomPage('Zelda no Densetsu - Kamigami no Triforce (Japan).sfc');
|
||||
|
||||
smrom := CheckRom('Super Metroid (JU).sfc', '21f3e98df4780ee1c667b84e57d88675');
|
||||
if Length(smrom) = 0 then
|
||||
SMRomFilePage:= AddRomPage('Super Metroid (JU).sfc');
|
||||
|
||||
dkc3rom := CheckRom('Donkey Kong Country 3 - Dixie Kong''s Double Trouble! (USA) (En,Fr).sfc', '120abf304f0c40fe059f6a192ed4f947');
|
||||
if Length(dkc3rom) = 0 then
|
||||
DKC3RomFilePage:= AddRomPage('Donkey Kong Country 3 - Dixie Kong''s Double Trouble! (USA) (En,Fr).sfc');
|
||||
|
||||
smwrom := CheckRom('Super Mario World (USA).sfc', 'cdd3c8c37322978ca8669b34bc89c804');
|
||||
if Length(smwrom) = 0 then
|
||||
SMWRomFilePage:= AddRomPage('Super Mario World (USA).sfc');
|
||||
|
||||
soerom := CheckRom('Secret of Evermore (USA).sfc', '6e9c94511d04fac6e0a1e582c170be3a');
|
||||
if Length(soerom) = 0 then
|
||||
SoEROMFilePage:= AddRomPage('Secret of Evermore (USA).sfc');
|
||||
|
||||
zlrom := CheckSMSRom('Zillion (UE) [!].sms', 'd4bf9e7bcf9a48da53785d2ae7bc4270');
|
||||
if Length(zlrom) = 0 then
|
||||
ZlROMFilePage:= AddSMSRomPage('Zillion (UE) [!].sms');
|
||||
|
||||
redrom := CheckRom('Pokemon Red (UE) [S][!].gb','3d45c1ee9abd5738df46d2bdda8b57dc');
|
||||
if Length(redrom) = 0 then
|
||||
RedROMFilePage:= AddGBRomPage('Pokemon Red (UE) [S][!].gb');
|
||||
|
||||
bluerom := CheckRom('Pokemon Blue (UE) [S][!].gb','50927e843568814f7ed45ec4f944bd8b');
|
||||
if Length(bluerom) = 0 then
|
||||
BlueROMFilePage:= AddGBRomPage('Pokemon Blue (UE) [S][!].gb');
|
||||
|
||||
bn3rom := CheckRom('Mega Man Battle Network 3 - Blue Version (USA).gba','6fe31df0144759b34ad666badaacc442');
|
||||
if Length(bn3rom) = 0 then
|
||||
BN3ROMFilePage:= AddGBARomPage('Mega Man Battle Network 3 - Blue Version (USA).gba');
|
||||
|
||||
ladxrom := CheckRom('Legend of Zelda, The - Link''s Awakening DX (USA, Europe) (SGB Enhanced).gbc','07c211479386825042efb4ad31bb525f');
|
||||
if Length(ladxrom) = 0 then
|
||||
LADXROMFilePage:= AddGBRomPage('Legend of Zelda, The - Link''s Awakening DX (USA, Europe) (SGB Enhanced).gbc');
|
||||
|
||||
l2acrom := CheckRom('Lufia II - Rise of the Sinistrals (USA).sfc', '6efc477d6203ed2b3b9133c1cd9e9c5d');
|
||||
if Length(l2acrom) = 0 then
|
||||
L2ACROMFilePage:= AddRomPage('Lufia II - Rise of the Sinistrals (USA).sfc');
|
||||
|
||||
tlozrom := CheckNESROM('Legend of Zelda, The (U) (PRG0) [!].nes', '337bd6f1a1163df31bf2633665589ab0');
|
||||
if Length(tlozrom) = 0 then
|
||||
TLoZROMFilePage:= AddNESRomPage('Legend of Zelda, The (U) (PRG0) [!].nes');
|
||||
|
||||
advnrom := CheckSMSRom('ADVNTURE.BIN', '157bddb7192754a45372be196797f284');
|
||||
if Length(advnrom) = 0 then
|
||||
AdvnROMFilePage:= AddA26Page('ADVNTURE.BIN');
|
||||
end;
|
||||
|
||||
|
||||
function ShouldSkipPage(PageID: Integer): Boolean;
|
||||
begin
|
||||
Result := False;
|
||||
if (assigned(LttPROMFilePage)) and (PageID = LttPROMFilePage.ID) then
|
||||
Result := not (WizardIsComponentSelected('client/sni/lttp') or WizardIsComponentSelected('generator/lttp'));
|
||||
if (assigned(SMROMFilePage)) and (PageID = SMROMFilePage.ID) then
|
||||
Result := not (WizardIsComponentSelected('client/sni/sm') or WizardIsComponentSelected('generator/sm'));
|
||||
if (assigned(DKC3ROMFilePage)) and (PageID = DKC3ROMFilePage.ID) then
|
||||
Result := not (WizardIsComponentSelected('client/sni/dkc3') or WizardIsComponentSelected('generator/dkc3'));
|
||||
if (assigned(SMWROMFilePage)) and (PageID = SMWROMFilePage.ID) then
|
||||
Result := not (WizardIsComponentSelected('client/sni/smw') or WizardIsComponentSelected('generator/smw'));
|
||||
if (assigned(L2ACROMFilePage)) and (PageID = L2ACROMFilePage.ID) then
|
||||
Result := not (WizardIsComponentSelected('client/sni/l2ac') or WizardIsComponentSelected('generator/l2ac'));
|
||||
if (assigned(SoEROMFilePage)) and (PageID = SoEROMFilePage.ID) then
|
||||
Result := not (WizardIsComponentSelected('generator/soe'));
|
||||
if (assigned(OoTROMFilePage)) and (PageID = OoTROMFilePage.ID) then
|
||||
Result := not (WizardIsComponentSelected('generator/oot') or WizardIsComponentSelected('client/oot'));
|
||||
if (assigned(ZlROMFilePage)) and (PageID = ZlROMFilePage.ID) then
|
||||
Result := not (WizardIsComponentSelected('generator/zl') or WizardIsComponentSelected('client/zl'));
|
||||
if (assigned(RedROMFilePage)) and (PageID = RedROMFilePage.ID) then
|
||||
Result := not (WizardIsComponentSelected('generator/pkmn_r') or WizardIsComponentSelected('client/pkmn/red'));
|
||||
if (assigned(BlueROMFilePage)) and (PageID = BlueROMFilePage.ID) then
|
||||
Result := not (WizardIsComponentSelected('generator/pkmn_b') or WizardIsComponentSelected('client/pkmn/blue'));
|
||||
if (assigned(BN3ROMFilePage)) and (PageID = BN3ROMFilePage.ID) then
|
||||
Result := not (WizardIsComponentSelected('generator/mmbn3') or WizardIsComponentSelected('client/mmbn3'));
|
||||
if (assigned(LADXROMFilePage)) and (PageID = LADXROMFilePage.ID) then
|
||||
Result := not (WizardIsComponentSelected('generator/ladx') or WizardIsComponentSelected('client/ladx'));
|
||||
if (assigned(TLoZROMFilePage)) and (PageID = TLoZROMFilePage.ID) then
|
||||
Result := not (WizardIsComponentSelected('generator/tloz') or WizardIsComponentSelected('client/tloz'));
|
||||
if (assigned(AdvnROMFilePage)) and (PageID = AdvnROMFilePage.ID) then
|
||||
Result := not (WizardIsComponentSelected('client/advn'));
|
||||
end;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
[pytest]
|
||||
python_files = Test*.py
|
||||
python_files = test_*.py Test*.py # TODO: remove Test* once all worlds have been ported
|
||||
python_classes = Test
|
||||
python_functions = test
|
||||
python_functions = test
|
||||
|
||||
20
settings.py
@@ -694,6 +694,25 @@ does nothing if not found
|
||||
snes_rom_start: Union[SnesRomStart, bool] = True
|
||||
|
||||
|
||||
class BizHawkClientOptions(Group):
|
||||
class EmuHawkPath(UserFilePath):
|
||||
"""
|
||||
The location of the EmuHawk you want to auto launch patched ROMs with
|
||||
"""
|
||||
is_exe = True
|
||||
description = "EmuHawk Executable"
|
||||
|
||||
class RomStart(str):
|
||||
"""
|
||||
Set this to true to autostart a patched ROM in BizHawk with the connector script,
|
||||
to false to never open the patched rom automatically,
|
||||
or to a path to an external program to open the ROM file with that instead.
|
||||
"""
|
||||
|
||||
emuhawk_path: EmuHawkPath = EmuHawkPath(None)
|
||||
rom_start: Union[RomStart, bool] = True
|
||||
|
||||
|
||||
# Top-level group with lazy loading of worlds
|
||||
|
||||
class Settings(Group):
|
||||
@@ -701,6 +720,7 @@ class Settings(Group):
|
||||
server_options: ServerOptions = ServerOptions()
|
||||
generator: GeneratorOptions = GeneratorOptions()
|
||||
sni_options: SNIOptions = SNIOptions()
|
||||
bizhawkclient_options: BizHawkClientOptions = BizHawkClientOptions()
|
||||
|
||||
_filename: Optional[str] = None
|
||||
|
||||
|
||||
1
setup.py
@@ -71,7 +71,6 @@ non_apworlds: set = {
|
||||
"Clique",
|
||||
"DLCQuest",
|
||||
"Final Fantasy",
|
||||
"Hylics 2",
|
||||
"Kingdom Hearts 2",
|
||||
"Lufia II Ancient Cave",
|
||||
"Meritous",
|
||||
|
||||
314
test/TestBase.py
@@ -1,311 +1,3 @@
|
||||
import typing
|
||||
import unittest
|
||||
from argparse import Namespace
|
||||
|
||||
from test.general import gen_steps
|
||||
from worlds import AutoWorld
|
||||
from worlds.AutoWorld import call_all
|
||||
|
||||
from BaseClasses import Location, MultiWorld, CollectionState, ItemClassification, Item
|
||||
from worlds.alttp.Items import ItemFactory
|
||||
|
||||
|
||||
class TestBase(unittest.TestCase):
|
||||
multiworld: MultiWorld
|
||||
_state_cache = {}
|
||||
|
||||
def get_state(self, items):
|
||||
if (self.multiworld, tuple(items)) in self._state_cache:
|
||||
return self._state_cache[self.multiworld, tuple(items)]
|
||||
state = CollectionState(self.multiworld)
|
||||
for item in items:
|
||||
item.classification = ItemClassification.progression
|
||||
state.collect(item, event=True)
|
||||
state.sweep_for_events()
|
||||
state.update_reachable_regions(1)
|
||||
self._state_cache[self.multiworld, tuple(items)] = state
|
||||
return state
|
||||
|
||||
def get_path(self, state, region):
|
||||
def flist_to_iter(node):
|
||||
while node:
|
||||
value, node = node
|
||||
yield value
|
||||
|
||||
from itertools import zip_longest
|
||||
reversed_path_as_flist = state.path.get(region, (region, None))
|
||||
string_path_flat = reversed(list(map(str, flist_to_iter(reversed_path_as_flist))))
|
||||
# Now we combine the flat string list into (region, exit) pairs
|
||||
pathsiter = iter(string_path_flat)
|
||||
pathpairs = zip_longest(pathsiter, pathsiter)
|
||||
return list(pathpairs)
|
||||
|
||||
def run_location_tests(self, access_pool):
|
||||
for i, (location, access, *item_pool) in enumerate(access_pool):
|
||||
items = item_pool[0]
|
||||
all_except = item_pool[1] if len(item_pool) > 1 else None
|
||||
state = self._get_items(item_pool, all_except)
|
||||
path = self.get_path(state, self.multiworld.get_location(location, 1).parent_region)
|
||||
with self.subTest(msg="Reach Location", location=location, access=access, items=items,
|
||||
all_except=all_except, path=path, entry=i):
|
||||
|
||||
self.assertEqual(self.multiworld.get_location(location, 1).can_reach(state), access,
|
||||
f"failed {self.multiworld.get_location(location, 1)} with: {item_pool}")
|
||||
|
||||
# check for partial solution
|
||||
if not all_except and access: # we are not supposed to be able to reach location with partial inventory
|
||||
for missing_item in item_pool[0]:
|
||||
with self.subTest(msg="Location reachable without required item", location=location,
|
||||
items=item_pool[0], missing_item=missing_item, entry=i):
|
||||
state = self._get_items_partial(item_pool, missing_item)
|
||||
|
||||
self.assertEqual(self.multiworld.get_location(location, 1).can_reach(state), False,
|
||||
f"failed {self.multiworld.get_location(location, 1)}: succeeded with "
|
||||
f"{missing_item} removed from: {item_pool}")
|
||||
|
||||
def run_entrance_tests(self, access_pool):
|
||||
for i, (entrance, access, *item_pool) in enumerate(access_pool):
|
||||
items = item_pool[0]
|
||||
all_except = item_pool[1] if len(item_pool) > 1 else None
|
||||
state = self._get_items(item_pool, all_except)
|
||||
path = self.get_path(state, self.multiworld.get_entrance(entrance, 1).parent_region)
|
||||
with self.subTest(msg="Reach Entrance", entrance=entrance, access=access, items=items,
|
||||
all_except=all_except, path=path, entry=i):
|
||||
|
||||
self.assertEqual(self.multiworld.get_entrance(entrance, 1).can_reach(state), access)
|
||||
|
||||
# check for partial solution
|
||||
if not all_except and access: # we are not supposed to be able to reach location with partial inventory
|
||||
for missing_item in item_pool[0]:
|
||||
with self.subTest(msg="Entrance reachable without required item", entrance=entrance,
|
||||
items=item_pool[0], missing_item=missing_item, entry=i):
|
||||
state = self._get_items_partial(item_pool, missing_item)
|
||||
self.assertEqual(self.multiworld.get_entrance(entrance, 1).can_reach(state), False,
|
||||
f"failed {self.multiworld.get_entrance(entrance, 1)} with: {item_pool}")
|
||||
|
||||
def _get_items(self, item_pool, all_except):
|
||||
if all_except and len(all_except) > 0:
|
||||
items = self.multiworld.itempool[:]
|
||||
items = [item for item in items if
|
||||
item.name not in all_except and not ("Bottle" in item.name and "AnyBottle" in all_except)]
|
||||
items.extend(ItemFactory(item_pool[0], 1))
|
||||
else:
|
||||
items = ItemFactory(item_pool[0], 1)
|
||||
return self.get_state(items)
|
||||
|
||||
def _get_items_partial(self, item_pool, missing_item):
|
||||
new_items = item_pool[0].copy()
|
||||
new_items.remove(missing_item)
|
||||
items = ItemFactory(new_items, 1)
|
||||
return self.get_state(items)
|
||||
|
||||
|
||||
class WorldTestBase(unittest.TestCase):
|
||||
options: typing.Dict[str, typing.Any] = {}
|
||||
multiworld: MultiWorld
|
||||
|
||||
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 """
|
||||
|
||||
def setUp(self) -> None:
|
||||
if self.auto_construct:
|
||||
self.world_setup()
|
||||
|
||||
def world_setup(self, seed: typing.Optional[int] = None) -> None:
|
||||
if type(self) is WorldTestBase or \
|
||||
(hasattr(WorldTestBase, self._testMethodName)
|
||||
and not self.run_default_tests and
|
||||
getattr(self, self._testMethodName).__code__ is
|
||||
getattr(WorldTestBase, self._testMethodName, None).__code__):
|
||||
return # setUp gets called for tests defined in the base class. We skip world_setup here.
|
||||
if not hasattr(self, "game"):
|
||||
raise NotImplementedError("didn't define game name")
|
||||
self.multiworld = MultiWorld(1)
|
||||
self.multiworld.game[1] = self.game
|
||||
self.multiworld.player_name = {1: "Tester"}
|
||||
self.multiworld.set_seed(seed)
|
||||
self.multiworld.state = CollectionState(self.multiworld)
|
||||
args = Namespace()
|
||||
for name, option in AutoWorld.AutoWorldRegister.world_types[self.game].options_dataclass.type_hints.items():
|
||||
setattr(args, name, {
|
||||
1: option.from_any(self.options.get(name, getattr(option, "default")))
|
||||
})
|
||||
self.multiworld.set_options(args)
|
||||
for step in gen_steps:
|
||||
call_all(self.multiworld, step)
|
||||
|
||||
# methods that can be called within tests
|
||||
def collect_all_but(self, item_names: typing.Union[str, typing.Iterable[str]],
|
||||
state: typing.Optional[CollectionState] = None) -> None:
|
||||
"""Collects all pre-placed items and items in the multiworld itempool except those provided"""
|
||||
if isinstance(item_names, str):
|
||||
item_names = (item_names,)
|
||||
if not state:
|
||||
state = self.multiworld.state
|
||||
for item in self.multiworld.get_items():
|
||||
if item.name not in item_names:
|
||||
state.collect(item)
|
||||
|
||||
def get_item_by_name(self, item_name: str) -> Item:
|
||||
"""Returns the first item found in placed items, or in the itempool with the matching name"""
|
||||
for item in self.multiworld.get_items():
|
||||
if item.name == item_name:
|
||||
return item
|
||||
raise ValueError("No such item")
|
||||
|
||||
def get_items_by_name(self, item_names: typing.Union[str, typing.Iterable[str]]) -> typing.List[Item]:
|
||||
"""Returns actual items from the itempool that match the provided name(s)"""
|
||||
if isinstance(item_names, str):
|
||||
item_names = (item_names,)
|
||||
return [item for item in self.multiworld.itempool if item.name in item_names]
|
||||
|
||||
def collect_by_name(self, item_names: typing.Union[str, typing.Iterable[str]]) -> typing.List[Item]:
|
||||
""" collect all of the items in the item pool that have the given names """
|
||||
items = self.get_items_by_name(item_names)
|
||||
self.collect(items)
|
||||
return items
|
||||
|
||||
def collect(self, items: typing.Union[Item, typing.Iterable[Item]]) -> None:
|
||||
"""Collects the provided item(s) into state"""
|
||||
if isinstance(items, Item):
|
||||
items = (items,)
|
||||
for item in items:
|
||||
self.multiworld.state.collect(item)
|
||||
|
||||
def remove_by_name(self, item_names: typing.Union[str, typing.Iterable[str]]) -> typing.List[Item]:
|
||||
"""Remove all of the items in the item pool with the given names from state"""
|
||||
items = self.get_items_by_name(item_names)
|
||||
self.remove(items)
|
||||
return items
|
||||
|
||||
def remove(self, items: typing.Union[Item, typing.Iterable[Item]]) -> None:
|
||||
"""Removes the provided item(s) from state"""
|
||||
if isinstance(items, Item):
|
||||
items = (items,)
|
||||
for item in items:
|
||||
if item.location and item.location.event and item.location in self.multiworld.state.events:
|
||||
self.multiworld.state.events.remove(item.location)
|
||||
self.multiworld.state.remove(item)
|
||||
|
||||
def can_reach_location(self, location: str) -> bool:
|
||||
"""Determines if the current state can reach the provided location name"""
|
||||
return self.multiworld.state.can_reach(location, "Location", 1)
|
||||
|
||||
def can_reach_entrance(self, entrance: str) -> bool:
|
||||
"""Determines if the current state can reach the provided entrance name"""
|
||||
return self.multiworld.state.can_reach(entrance, "Entrance", 1)
|
||||
|
||||
def can_reach_region(self, region: str) -> bool:
|
||||
"""Determines if the current state can reach the provided region name"""
|
||||
return self.multiworld.state.can_reach(region, "Region", 1)
|
||||
|
||||
def count(self, item_name: str) -> int:
|
||||
"""Returns the amount of an item currently in state"""
|
||||
return self.multiworld.state.count(item_name, 1)
|
||||
|
||||
def assertAccessDependency(self,
|
||||
locations: typing.List[str],
|
||||
possible_items: typing.Iterable[typing.Iterable[str]],
|
||||
only_check_listed: bool = False) -> None:
|
||||
"""Asserts that the provided locations can't be reached without the listed items but can be reached with any
|
||||
one of the provided combinations"""
|
||||
all_items = [item_name for item_names in possible_items for item_name in item_names]
|
||||
|
||||
state = CollectionState(self.multiworld)
|
||||
self.collect_all_but(all_items, state)
|
||||
if only_check_listed:
|
||||
for location in locations:
|
||||
self.assertFalse(state.can_reach(location, "Location", 1), f"{location} is reachable without {all_items}")
|
||||
else:
|
||||
for location in self.multiworld.get_locations():
|
||||
loc_reachable = state.can_reach(location, "Location", 1)
|
||||
self.assertEqual(loc_reachable, location.name not in locations,
|
||||
f"{location.name} is reachable without {all_items}" if loc_reachable
|
||||
else f"{location.name} is not reachable without {all_items}")
|
||||
for item_names in possible_items:
|
||||
items = self.get_items_by_name(item_names)
|
||||
for item in items:
|
||||
state.collect(item)
|
||||
for location in locations:
|
||||
self.assertTrue(state.can_reach(location, "Location", 1),
|
||||
f"{location} not reachable with {item_names}")
|
||||
for item in items:
|
||||
state.remove(item)
|
||||
|
||||
def assertBeatable(self, beatable: bool):
|
||||
"""Asserts that the game can be beaten with the current state"""
|
||||
self.assertEqual(self.multiworld.can_beat_game(self.multiworld.state), beatable)
|
||||
|
||||
# following tests are automatically run
|
||||
@property
|
||||
def run_default_tests(self) -> bool:
|
||||
"""Not possible or identical to the base test that's always being run already"""
|
||||
return (self.options
|
||||
or self.setUp.__code__ is not WorldTestBase.setUp.__code__
|
||||
or self.world_setup.__code__ is not WorldTestBase.world_setup.__code__)
|
||||
|
||||
@property
|
||||
def constructed(self) -> bool:
|
||||
"""A multiworld has been constructed by this point"""
|
||||
return hasattr(self, "game") and hasattr(self, "multiworld")
|
||||
|
||||
def testAllStateCanReachEverything(self):
|
||||
"""Ensure all state can reach everything and complete the game with the defined options"""
|
||||
if not (self.run_default_tests and self.constructed):
|
||||
return
|
||||
with self.subTest("Game", game=self.game):
|
||||
excluded = self.multiworld.exclude_locations[1].value
|
||||
state = self.multiworld.get_all_state(False)
|
||||
for location in self.multiworld.get_locations():
|
||||
if location.name not in excluded:
|
||||
with self.subTest("Location should be reached", location=location):
|
||||
reachable = location.can_reach(state)
|
||||
self.assertTrue(reachable, f"{location.name} unreachable")
|
||||
with self.subTest("Beatable"):
|
||||
self.multiworld.state = state
|
||||
self.assertBeatable(True)
|
||||
|
||||
def testEmptyStateCanReachSomething(self):
|
||||
"""Ensure empty state can reach at least one location with the defined options"""
|
||||
if not (self.run_default_tests and self.constructed):
|
||||
return
|
||||
with self.subTest("Game", game=self.game):
|
||||
state = CollectionState(self.multiworld)
|
||||
locations = self.multiworld.get_reachable_locations(state, 1)
|
||||
self.assertGreater(len(locations), 0,
|
||||
"Need to be able to reach at least one location to get started.")
|
||||
|
||||
def testFill(self):
|
||||
"""Generates a multiworld and validates placements with the defined options"""
|
||||
# don't run this test if accessibility is set manually
|
||||
if not (self.run_default_tests and self.constructed):
|
||||
return
|
||||
from Fill import distribute_items_restrictive
|
||||
|
||||
# basically a shortened reimplementation of this method from core, in order to force the check is done
|
||||
def fulfills_accessibility():
|
||||
locations = self.multiworld.get_locations(1).copy()
|
||||
state = CollectionState(self.multiworld)
|
||||
while locations:
|
||||
sphere: typing.List[Location] = []
|
||||
for n in range(len(locations) - 1, -1, -1):
|
||||
if locations[n].can_reach(state):
|
||||
sphere.append(locations.pop(n))
|
||||
self.assertTrue(sphere or self.multiworld.accessibility[1] == "minimal",
|
||||
f"Unreachable locations: {locations}")
|
||||
if not sphere:
|
||||
break
|
||||
for location in sphere:
|
||||
if location.item:
|
||||
state.collect(location.item, True, location)
|
||||
|
||||
return self.multiworld.has_beaten_game(state, 1)
|
||||
|
||||
with self.subTest("Game", game=self.game):
|
||||
distribute_items_restrictive(self.multiworld)
|
||||
call_all(self.multiworld, "post_fill")
|
||||
self.assertTrue(fulfills_accessibility(), "Collected all locations, but can't beat the game.")
|
||||
placed_items = [loc.item for loc in self.multiworld.get_locations() if loc.item and loc.item.code]
|
||||
self.assertLessEqual(len(self.multiworld.itempool), len(placed_items),
|
||||
"Unplaced Items remaining in itempool")
|
||||
from .bases import TestBase, WorldTestBase
|
||||
from warnings import warn
|
||||
warn("TestBase was renamed to bases", DeprecationWarning)
|
||||
|
||||
309
test/bases.py
Normal file
@@ -0,0 +1,309 @@
|
||||
import typing
|
||||
import unittest
|
||||
from argparse import Namespace
|
||||
|
||||
from test.general import gen_steps
|
||||
from worlds import AutoWorld
|
||||
from worlds.AutoWorld import call_all
|
||||
|
||||
from BaseClasses import Location, MultiWorld, CollectionState, ItemClassification, Item
|
||||
from worlds.alttp.Items import ItemFactory
|
||||
|
||||
|
||||
class TestBase(unittest.TestCase):
|
||||
multiworld: MultiWorld
|
||||
_state_cache = {}
|
||||
|
||||
def get_state(self, items):
|
||||
if (self.multiworld, tuple(items)) in self._state_cache:
|
||||
return self._state_cache[self.multiworld, tuple(items)]
|
||||
state = CollectionState(self.multiworld)
|
||||
for item in items:
|
||||
item.classification = ItemClassification.progression
|
||||
state.collect(item, event=True)
|
||||
state.sweep_for_events()
|
||||
state.update_reachable_regions(1)
|
||||
self._state_cache[self.multiworld, tuple(items)] = state
|
||||
return state
|
||||
|
||||
def get_path(self, state, region):
|
||||
def flist_to_iter(node):
|
||||
while node:
|
||||
value, node = node
|
||||
yield value
|
||||
|
||||
from itertools import zip_longest
|
||||
reversed_path_as_flist = state.path.get(region, (region, None))
|
||||
string_path_flat = reversed(list(map(str, flist_to_iter(reversed_path_as_flist))))
|
||||
# Now we combine the flat string list into (region, exit) pairs
|
||||
pathsiter = iter(string_path_flat)
|
||||
pathpairs = zip_longest(pathsiter, pathsiter)
|
||||
return list(pathpairs)
|
||||
|
||||
def run_location_tests(self, access_pool):
|
||||
for i, (location, access, *item_pool) in enumerate(access_pool):
|
||||
items = item_pool[0]
|
||||
all_except = item_pool[1] if len(item_pool) > 1 else None
|
||||
state = self._get_items(item_pool, all_except)
|
||||
path = self.get_path(state, self.multiworld.get_location(location, 1).parent_region)
|
||||
with self.subTest(msg="Reach Location", location=location, access=access, items=items,
|
||||
all_except=all_except, path=path, entry=i):
|
||||
|
||||
self.assertEqual(self.multiworld.get_location(location, 1).can_reach(state), access,
|
||||
f"failed {self.multiworld.get_location(location, 1)} with: {item_pool}")
|
||||
|
||||
# check for partial solution
|
||||
if not all_except and access: # we are not supposed to be able to reach location with partial inventory
|
||||
for missing_item in item_pool[0]:
|
||||
with self.subTest(msg="Location reachable without required item", location=location,
|
||||
items=item_pool[0], missing_item=missing_item, entry=i):
|
||||
state = self._get_items_partial(item_pool, missing_item)
|
||||
|
||||
self.assertEqual(self.multiworld.get_location(location, 1).can_reach(state), False,
|
||||
f"failed {self.multiworld.get_location(location, 1)}: succeeded with "
|
||||
f"{missing_item} removed from: {item_pool}")
|
||||
|
||||
def run_entrance_tests(self, access_pool):
|
||||
for i, (entrance, access, *item_pool) in enumerate(access_pool):
|
||||
items = item_pool[0]
|
||||
all_except = item_pool[1] if len(item_pool) > 1 else None
|
||||
state = self._get_items(item_pool, all_except)
|
||||
path = self.get_path(state, self.multiworld.get_entrance(entrance, 1).parent_region)
|
||||
with self.subTest(msg="Reach Entrance", entrance=entrance, access=access, items=items,
|
||||
all_except=all_except, path=path, entry=i):
|
||||
|
||||
self.assertEqual(self.multiworld.get_entrance(entrance, 1).can_reach(state), access)
|
||||
|
||||
# check for partial solution
|
||||
if not all_except and access: # we are not supposed to be able to reach location with partial inventory
|
||||
for missing_item in item_pool[0]:
|
||||
with self.subTest(msg="Entrance reachable without required item", entrance=entrance,
|
||||
items=item_pool[0], missing_item=missing_item, entry=i):
|
||||
state = self._get_items_partial(item_pool, missing_item)
|
||||
self.assertEqual(self.multiworld.get_entrance(entrance, 1).can_reach(state), False,
|
||||
f"failed {self.multiworld.get_entrance(entrance, 1)} with: {item_pool}")
|
||||
|
||||
def _get_items(self, item_pool, all_except):
|
||||
if all_except and len(all_except) > 0:
|
||||
items = self.multiworld.itempool[:]
|
||||
items = [item for item in items if
|
||||
item.name not in all_except and not ("Bottle" in item.name and "AnyBottle" in all_except)]
|
||||
items.extend(ItemFactory(item_pool[0], 1))
|
||||
else:
|
||||
items = ItemFactory(item_pool[0], 1)
|
||||
return self.get_state(items)
|
||||
|
||||
def _get_items_partial(self, item_pool, missing_item):
|
||||
new_items = item_pool[0].copy()
|
||||
new_items.remove(missing_item)
|
||||
items = ItemFactory(new_items, 1)
|
||||
return self.get_state(items)
|
||||
|
||||
|
||||
class WorldTestBase(unittest.TestCase):
|
||||
options: typing.Dict[str, typing.Any] = {}
|
||||
multiworld: MultiWorld
|
||||
|
||||
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 """
|
||||
|
||||
def setUp(self) -> None:
|
||||
if self.auto_construct:
|
||||
self.world_setup()
|
||||
|
||||
def world_setup(self, seed: typing.Optional[int] = None) -> None:
|
||||
if type(self) is WorldTestBase or \
|
||||
(hasattr(WorldTestBase, self._testMethodName)
|
||||
and not self.run_default_tests and
|
||||
getattr(self, self._testMethodName).__code__ is
|
||||
getattr(WorldTestBase, self._testMethodName, None).__code__):
|
||||
return # setUp gets called for tests defined in the base class. We skip world_setup here.
|
||||
if not hasattr(self, "game"):
|
||||
raise NotImplementedError("didn't define game name")
|
||||
self.multiworld = MultiWorld(1)
|
||||
self.multiworld.game[1] = self.game
|
||||
self.multiworld.player_name = {1: "Tester"}
|
||||
self.multiworld.set_seed(seed)
|
||||
self.multiworld.state = CollectionState(self.multiworld)
|
||||
args = Namespace()
|
||||
for name, option in AutoWorld.AutoWorldRegister.world_types[self.game].options_dataclass.type_hints.items():
|
||||
setattr(args, name, {
|
||||
1: option.from_any(self.options.get(name, getattr(option, "default")))
|
||||
})
|
||||
self.multiworld.set_options(args)
|
||||
for step in gen_steps:
|
||||
call_all(self.multiworld, step)
|
||||
|
||||
# methods that can be called within tests
|
||||
def collect_all_but(self, item_names: typing.Union[str, typing.Iterable[str]],
|
||||
state: typing.Optional[CollectionState] = None) -> None:
|
||||
"""Collects all pre-placed items and items in the multiworld itempool except those provided"""
|
||||
if isinstance(item_names, str):
|
||||
item_names = (item_names,)
|
||||
if not state:
|
||||
state = self.multiworld.state
|
||||
for item in self.multiworld.get_items():
|
||||
if item.name not in item_names:
|
||||
state.collect(item)
|
||||
|
||||
def get_item_by_name(self, item_name: str) -> Item:
|
||||
"""Returns the first item found in placed items, or in the itempool with the matching name"""
|
||||
for item in self.multiworld.get_items():
|
||||
if item.name == item_name:
|
||||
return item
|
||||
raise ValueError("No such item")
|
||||
|
||||
def get_items_by_name(self, item_names: typing.Union[str, typing.Iterable[str]]) -> typing.List[Item]:
|
||||
"""Returns actual items from the itempool that match the provided name(s)"""
|
||||
if isinstance(item_names, str):
|
||||
item_names = (item_names,)
|
||||
return [item for item in self.multiworld.itempool if item.name in item_names]
|
||||
|
||||
def collect_by_name(self, item_names: typing.Union[str, typing.Iterable[str]]) -> typing.List[Item]:
|
||||
""" collect all of the items in the item pool that have the given names """
|
||||
items = self.get_items_by_name(item_names)
|
||||
self.collect(items)
|
||||
return items
|
||||
|
||||
def collect(self, items: typing.Union[Item, typing.Iterable[Item]]) -> None:
|
||||
"""Collects the provided item(s) into state"""
|
||||
if isinstance(items, Item):
|
||||
items = (items,)
|
||||
for item in items:
|
||||
self.multiworld.state.collect(item)
|
||||
|
||||
def remove_by_name(self, item_names: typing.Union[str, typing.Iterable[str]]) -> typing.List[Item]:
|
||||
"""Remove all of the items in the item pool with the given names from state"""
|
||||
items = self.get_items_by_name(item_names)
|
||||
self.remove(items)
|
||||
return items
|
||||
|
||||
def remove(self, items: typing.Union[Item, typing.Iterable[Item]]) -> None:
|
||||
"""Removes the provided item(s) from state"""
|
||||
if isinstance(items, Item):
|
||||
items = (items,)
|
||||
for item in items:
|
||||
if item.location and item.location.event and item.location in self.multiworld.state.events:
|
||||
self.multiworld.state.events.remove(item.location)
|
||||
self.multiworld.state.remove(item)
|
||||
|
||||
def can_reach_location(self, location: str) -> bool:
|
||||
"""Determines if the current state can reach the provided location name"""
|
||||
return self.multiworld.state.can_reach(location, "Location", 1)
|
||||
|
||||
def can_reach_entrance(self, entrance: str) -> bool:
|
||||
"""Determines if the current state can reach the provided entrance name"""
|
||||
return self.multiworld.state.can_reach(entrance, "Entrance", 1)
|
||||
|
||||
def can_reach_region(self, region: str) -> bool:
|
||||
"""Determines if the current state can reach the provided region name"""
|
||||
return self.multiworld.state.can_reach(region, "Region", 1)
|
||||
|
||||
def count(self, item_name: str) -> int:
|
||||
"""Returns the amount of an item currently in state"""
|
||||
return self.multiworld.state.count(item_name, 1)
|
||||
|
||||
def assertAccessDependency(self,
|
||||
locations: typing.List[str],
|
||||
possible_items: typing.Iterable[typing.Iterable[str]],
|
||||
only_check_listed: bool = False) -> None:
|
||||
"""Asserts that the provided locations can't be reached without the listed items but can be reached with any
|
||||
one of the provided combinations"""
|
||||
all_items = [item_name for item_names in possible_items for item_name in item_names]
|
||||
|
||||
state = CollectionState(self.multiworld)
|
||||
self.collect_all_but(all_items, state)
|
||||
if only_check_listed:
|
||||
for location in locations:
|
||||
self.assertFalse(state.can_reach(location, "Location", 1), f"{location} is reachable without {all_items}")
|
||||
else:
|
||||
for location in self.multiworld.get_locations():
|
||||
loc_reachable = state.can_reach(location, "Location", 1)
|
||||
self.assertEqual(loc_reachable, location.name not in locations,
|
||||
f"{location.name} is reachable without {all_items}" if loc_reachable
|
||||
else f"{location.name} is not reachable without {all_items}")
|
||||
for item_names in possible_items:
|
||||
items = self.get_items_by_name(item_names)
|
||||
for item in items:
|
||||
state.collect(item)
|
||||
for location in locations:
|
||||
self.assertTrue(state.can_reach(location, "Location", 1),
|
||||
f"{location} not reachable with {item_names}")
|
||||
for item in items:
|
||||
state.remove(item)
|
||||
|
||||
def assertBeatable(self, beatable: bool):
|
||||
"""Asserts that the game can be beaten with the current state"""
|
||||
self.assertEqual(self.multiworld.can_beat_game(self.multiworld.state), beatable)
|
||||
|
||||
# following tests are automatically run
|
||||
@property
|
||||
def run_default_tests(self) -> bool:
|
||||
"""Not possible or identical to the base test that's always being run already"""
|
||||
return (self.options
|
||||
or self.setUp.__code__ is not WorldTestBase.setUp.__code__
|
||||
or self.world_setup.__code__ is not WorldTestBase.world_setup.__code__)
|
||||
|
||||
@property
|
||||
def constructed(self) -> bool:
|
||||
"""A multiworld has been constructed by this point"""
|
||||
return hasattr(self, "game") and hasattr(self, "multiworld")
|
||||
|
||||
def test_all_state_can_reach_everything(self):
|
||||
"""Ensure all state can reach everything and complete the game with the defined options"""
|
||||
if not (self.run_default_tests and self.constructed):
|
||||
return
|
||||
with self.subTest("Game", game=self.game):
|
||||
excluded = self.multiworld.exclude_locations[1].value
|
||||
state = self.multiworld.get_all_state(False)
|
||||
for location in self.multiworld.get_locations():
|
||||
if location.name not in excluded:
|
||||
with self.subTest("Location should be reached", location=location):
|
||||
reachable = location.can_reach(state)
|
||||
self.assertTrue(reachable, f"{location.name} unreachable")
|
||||
with self.subTest("Beatable"):
|
||||
self.multiworld.state = state
|
||||
self.assertBeatable(True)
|
||||
|
||||
def test_empty_state_can_reach_something(self):
|
||||
"""Ensure empty state can reach at least one location with the defined options"""
|
||||
if not (self.run_default_tests and self.constructed):
|
||||
return
|
||||
with self.subTest("Game", game=self.game):
|
||||
state = CollectionState(self.multiworld)
|
||||
locations = self.multiworld.get_reachable_locations(state, 1)
|
||||
self.assertGreater(len(locations), 0,
|
||||
"Need to be able to reach at least one location to get started.")
|
||||
|
||||
def test_fill(self):
|
||||
"""Generates a multiworld and validates placements with the defined options"""
|
||||
if not (self.run_default_tests and self.constructed):
|
||||
return
|
||||
from Fill import distribute_items_restrictive
|
||||
|
||||
# basically a shortened reimplementation of this method from core, in order to force the check is done
|
||||
def fulfills_accessibility() -> bool:
|
||||
locations = self.multiworld.get_locations(1).copy()
|
||||
state = CollectionState(self.multiworld)
|
||||
while locations:
|
||||
sphere: typing.List[Location] = []
|
||||
for n in range(len(locations) - 1, -1, -1):
|
||||
if locations[n].can_reach(state):
|
||||
sphere.append(locations.pop(n))
|
||||
self.assertTrue(sphere or self.multiworld.accessibility[1] == "minimal",
|
||||
f"Unreachable locations: {locations}")
|
||||
if not sphere:
|
||||
break
|
||||
for location in sphere:
|
||||
if location.item:
|
||||
state.collect(location.item, True, location)
|
||||
return self.multiworld.has_beaten_game(state, 1)
|
||||
|
||||
with self.subTest("Game", game=self.game, seed=self.multiworld.seed):
|
||||
distribute_items_restrictive(self.multiworld)
|
||||
call_all(self.multiworld, "post_fill")
|
||||
self.assertTrue(fulfills_accessibility(), "Collected all locations, but can't beat the game.")
|
||||
placed_items = [loc.item for loc in self.multiworld.get_locations() if loc.item and loc.item.code]
|
||||
self.assertLessEqual(len(self.multiworld.itempool), len(placed_items),
|
||||
"Unplaced Items remaining in itempool")
|
||||
@@ -8,6 +8,13 @@ gen_steps = ("generate_early", "create_regions", "create_items", "set_rules", "g
|
||||
|
||||
|
||||
def setup_solo_multiworld(world_type: Type[World], steps: Tuple[str, ...] = gen_steps) -> MultiWorld:
|
||||
"""
|
||||
Creates a multiworld with a single player of `world_type`, sets default options, and calls provided gen steps.
|
||||
|
||||
:param world_type: Type of the world to generate a multiworld for
|
||||
:param steps: The gen steps that should be called on the generated multiworld before returning. Default calls
|
||||
steps through pre_fill
|
||||
"""
|
||||
multiworld = MultiWorld(1)
|
||||
multiworld.game[1] = world_type.game
|
||||
multiworld.player_name = {1: "Tester"}
|
||||
|
||||
@@ -72,7 +72,7 @@ class PlayerDefinition(object):
|
||||
return region
|
||||
|
||||
|
||||
def fillRegion(world: MultiWorld, region: Region, items: List[Item]) -> List[Item]:
|
||||
def fill_region(world: MultiWorld, region: Region, items: List[Item]) -> List[Item]:
|
||||
items = items.copy()
|
||||
while len(items) > 0:
|
||||
location = region.locations.pop(0)
|
||||
@@ -86,7 +86,7 @@ def fillRegion(world: MultiWorld, region: Region, items: List[Item]) -> List[Ite
|
||||
return items
|
||||
|
||||
|
||||
def regionContains(region: Region, item: Item) -> bool:
|
||||
def region_contains(region: Region, item: Item) -> bool:
|
||||
for location in region.locations:
|
||||
if location.item == item:
|
||||
return True
|
||||
@@ -133,6 +133,7 @@ def names(objs: list) -> Iterable[str]:
|
||||
|
||||
class TestFillRestrictive(unittest.TestCase):
|
||||
def test_basic_fill(self):
|
||||
"""Tests `fill_restrictive` fills and removes the locations and items from their respective lists"""
|
||||
multi_world = generate_multi_world()
|
||||
player1 = generate_player_data(multi_world, 1, 2, 2)
|
||||
|
||||
@@ -150,6 +151,7 @@ class TestFillRestrictive(unittest.TestCase):
|
||||
self.assertEqual([], player1.prog_items)
|
||||
|
||||
def test_ordered_fill(self):
|
||||
"""Tests `fill_restrictive` fulfills set rules"""
|
||||
multi_world = generate_multi_world()
|
||||
player1 = generate_player_data(multi_world, 1, 2, 2)
|
||||
items = player1.prog_items
|
||||
@@ -166,6 +168,7 @@ class TestFillRestrictive(unittest.TestCase):
|
||||
self.assertEqual(locations[1].item, items[1])
|
||||
|
||||
def test_partial_fill(self):
|
||||
"""Tests that `fill_restrictive` returns unfilled locations"""
|
||||
multi_world = generate_multi_world()
|
||||
player1 = generate_player_data(multi_world, 1, 3, 2)
|
||||
|
||||
@@ -191,6 +194,7 @@ class TestFillRestrictive(unittest.TestCase):
|
||||
self.assertEqual(player1.locations[0], loc2)
|
||||
|
||||
def test_minimal_fill(self):
|
||||
"""Test that fill for minimal player can have unreachable items"""
|
||||
multi_world = generate_multi_world()
|
||||
player1 = generate_player_data(multi_world, 1, 2, 2)
|
||||
|
||||
@@ -246,6 +250,7 @@ class TestFillRestrictive(unittest.TestCase):
|
||||
f'{item} is unreachable in {item.location}')
|
||||
|
||||
def test_reversed_fill(self):
|
||||
"""Test a different set of rules can be satisfied"""
|
||||
multi_world = generate_multi_world()
|
||||
player1 = generate_player_data(multi_world, 1, 2, 2)
|
||||
|
||||
@@ -264,6 +269,7 @@ class TestFillRestrictive(unittest.TestCase):
|
||||
self.assertEqual(loc1.item, item0)
|
||||
|
||||
def test_multi_step_fill(self):
|
||||
"""Test that fill is able to satisfy multiple spheres"""
|
||||
multi_world = generate_multi_world()
|
||||
player1 = generate_player_data(multi_world, 1, 4, 4)
|
||||
|
||||
@@ -288,6 +294,7 @@ class TestFillRestrictive(unittest.TestCase):
|
||||
self.assertEqual(locations[3].item, items[3])
|
||||
|
||||
def test_impossible_fill(self):
|
||||
"""Test that fill raises an error when it can't place any items"""
|
||||
multi_world = generate_multi_world()
|
||||
player1 = generate_player_data(multi_world, 1, 2, 2)
|
||||
items = player1.prog_items
|
||||
@@ -304,6 +311,7 @@ class TestFillRestrictive(unittest.TestCase):
|
||||
player1.locations.copy(), player1.prog_items.copy())
|
||||
|
||||
def test_circular_fill(self):
|
||||
"""Test that fill raises an error when it can't place all items"""
|
||||
multi_world = generate_multi_world()
|
||||
player1 = generate_player_data(multi_world, 1, 3, 3)
|
||||
|
||||
@@ -324,6 +332,7 @@ class TestFillRestrictive(unittest.TestCase):
|
||||
player1.locations.copy(), player1.prog_items.copy())
|
||||
|
||||
def test_competing_fill(self):
|
||||
"""Test that fill raises an error when it can't place items in a way to satisfy the conditions"""
|
||||
multi_world = generate_multi_world()
|
||||
player1 = generate_player_data(multi_world, 1, 2, 2)
|
||||
|
||||
@@ -340,6 +349,7 @@ class TestFillRestrictive(unittest.TestCase):
|
||||
player1.locations.copy(), player1.prog_items.copy())
|
||||
|
||||
def test_multiplayer_fill(self):
|
||||
"""Test that items can be placed across worlds"""
|
||||
multi_world = generate_multi_world(2)
|
||||
player1 = generate_player_data(multi_world, 1, 2, 2)
|
||||
player2 = generate_player_data(multi_world, 2, 2, 2)
|
||||
@@ -360,6 +370,7 @@ class TestFillRestrictive(unittest.TestCase):
|
||||
self.assertEqual(player2.locations[1].item, player2.prog_items[0])
|
||||
|
||||
def test_multiplayer_rules_fill(self):
|
||||
"""Test that fill across worlds satisfies the rules"""
|
||||
multi_world = generate_multi_world(2)
|
||||
player1 = generate_player_data(multi_world, 1, 2, 2)
|
||||
player2 = generate_player_data(multi_world, 2, 2, 2)
|
||||
@@ -383,6 +394,7 @@ class TestFillRestrictive(unittest.TestCase):
|
||||
self.assertEqual(player2.locations[1].item, player1.prog_items[1])
|
||||
|
||||
def test_restrictive_progress(self):
|
||||
"""Test that various spheres with different requirements can be filled"""
|
||||
multi_world = generate_multi_world()
|
||||
player1 = generate_player_data(multi_world, 1, prog_item_count=25)
|
||||
items = player1.prog_items.copy()
|
||||
@@ -405,6 +417,7 @@ class TestFillRestrictive(unittest.TestCase):
|
||||
locations, player1.prog_items)
|
||||
|
||||
def test_swap_to_earlier_location_with_item_rule(self):
|
||||
"""Test that item swap happens and works as intended"""
|
||||
# test for PR#1109
|
||||
multi_world = generate_multi_world(1)
|
||||
player1 = generate_player_data(multi_world, 1, 4, 4)
|
||||
@@ -430,6 +443,7 @@ class TestFillRestrictive(unittest.TestCase):
|
||||
self.assertEqual(sphere1_loc.item, allowed_item, "Wrong item in Sphere 1")
|
||||
|
||||
def test_double_sweep(self):
|
||||
"""Test that sweep doesn't duplicate Event items when sweeping"""
|
||||
# test for PR1114
|
||||
multi_world = generate_multi_world(1)
|
||||
player1 = generate_player_data(multi_world, 1, 1, 1)
|
||||
@@ -445,6 +459,7 @@ class TestFillRestrictive(unittest.TestCase):
|
||||
self.assertEqual(multi_world.state.prog_items[item.name, item.player], 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"""
|
||||
multi_world = generate_multi_world()
|
||||
player1 = generate_player_data(multi_world, 1, 2, 2)
|
||||
|
||||
@@ -461,6 +476,7 @@ class TestFillRestrictive(unittest.TestCase):
|
||||
|
||||
class TestDistributeItemsRestrictive(unittest.TestCase):
|
||||
def test_basic_distribute(self):
|
||||
"""Test that distribute_items_restrictive is deterministic"""
|
||||
multi_world = generate_multi_world()
|
||||
player1 = generate_player_data(
|
||||
multi_world, 1, 4, prog_item_count=2, basic_item_count=2)
|
||||
@@ -480,6 +496,7 @@ class TestDistributeItemsRestrictive(unittest.TestCase):
|
||||
self.assertFalse(locations[3].event)
|
||||
|
||||
def test_excluded_distribute(self):
|
||||
"""Test that distribute_items_restrictive doesn't put advancement items on excluded locations"""
|
||||
multi_world = generate_multi_world()
|
||||
player1 = generate_player_data(
|
||||
multi_world, 1, 4, prog_item_count=2, basic_item_count=2)
|
||||
@@ -494,6 +511,7 @@ class TestDistributeItemsRestrictive(unittest.TestCase):
|
||||
self.assertFalse(locations[2].item.advancement)
|
||||
|
||||
def test_non_excluded_item_distribute(self):
|
||||
"""Test that useful items aren't placed on excluded locations"""
|
||||
multi_world = generate_multi_world()
|
||||
player1 = generate_player_data(
|
||||
multi_world, 1, 4, prog_item_count=2, basic_item_count=2)
|
||||
@@ -508,6 +526,7 @@ class TestDistributeItemsRestrictive(unittest.TestCase):
|
||||
self.assertEqual(locations[1].item, basic_items[0])
|
||||
|
||||
def test_too_many_excluded_distribute(self):
|
||||
"""Test that fill fails if it can't place all progression items due to too many excluded locations"""
|
||||
multi_world = generate_multi_world()
|
||||
player1 = generate_player_data(
|
||||
multi_world, 1, 4, prog_item_count=2, basic_item_count=2)
|
||||
@@ -520,6 +539,7 @@ class TestDistributeItemsRestrictive(unittest.TestCase):
|
||||
self.assertRaises(FillError, distribute_items_restrictive, multi_world)
|
||||
|
||||
def test_non_excluded_item_must_distribute(self):
|
||||
"""Test that fill fails if it can't place useful items due to too many excluded locations"""
|
||||
multi_world = generate_multi_world()
|
||||
player1 = generate_player_data(
|
||||
multi_world, 1, 4, prog_item_count=2, basic_item_count=2)
|
||||
@@ -534,6 +554,7 @@ class TestDistributeItemsRestrictive(unittest.TestCase):
|
||||
self.assertRaises(FillError, distribute_items_restrictive, multi_world)
|
||||
|
||||
def test_priority_distribute(self):
|
||||
"""Test that priority locations receive advancement items"""
|
||||
multi_world = generate_multi_world()
|
||||
player1 = generate_player_data(
|
||||
multi_world, 1, 4, prog_item_count=2, basic_item_count=2)
|
||||
@@ -548,6 +569,7 @@ class TestDistributeItemsRestrictive(unittest.TestCase):
|
||||
self.assertTrue(locations[3].item.advancement)
|
||||
|
||||
def test_excess_priority_distribute(self):
|
||||
"""Test that if there's more priority locations than advancement items, they can still fill"""
|
||||
multi_world = generate_multi_world()
|
||||
player1 = generate_player_data(
|
||||
multi_world, 1, 4, prog_item_count=2, basic_item_count=2)
|
||||
@@ -562,6 +584,7 @@ class TestDistributeItemsRestrictive(unittest.TestCase):
|
||||
self.assertFalse(locations[3].item.advancement)
|
||||
|
||||
def test_multiple_world_priority_distribute(self):
|
||||
"""Test that priority fill can be satisfied for multiple worlds"""
|
||||
multi_world = generate_multi_world(3)
|
||||
player1 = generate_player_data(
|
||||
multi_world, 1, 4, prog_item_count=2, basic_item_count=2)
|
||||
@@ -591,7 +614,7 @@ class TestDistributeItemsRestrictive(unittest.TestCase):
|
||||
self.assertTrue(player3.locations[3].item.advancement)
|
||||
|
||||
def test_can_remove_locations_in_fill_hook(self):
|
||||
|
||||
"""Test that distribute_items_restrictive calls the fill hook and allows for item and location removal"""
|
||||
multi_world = generate_multi_world()
|
||||
player1 = generate_player_data(
|
||||
multi_world, 1, 4, prog_item_count=2, basic_item_count=2)
|
||||
@@ -611,6 +634,7 @@ class TestDistributeItemsRestrictive(unittest.TestCase):
|
||||
self.assertIsNone(removed_location[0].item)
|
||||
|
||||
def test_seed_robust_to_item_order(self):
|
||||
"""Test deterministic fill"""
|
||||
mw1 = generate_multi_world()
|
||||
gen1 = generate_player_data(
|
||||
mw1, 1, 4, prog_item_count=2, basic_item_count=2)
|
||||
@@ -628,6 +652,7 @@ class TestDistributeItemsRestrictive(unittest.TestCase):
|
||||
self.assertEqual(gen1.locations[3].item, gen2.locations[3].item)
|
||||
|
||||
def test_seed_robust_to_location_order(self):
|
||||
"""Test deterministic fill even if locations in a region are reordered"""
|
||||
mw1 = generate_multi_world()
|
||||
gen1 = generate_player_data(
|
||||
mw1, 1, 4, prog_item_count=2, basic_item_count=2)
|
||||
@@ -646,6 +671,7 @@ class TestDistributeItemsRestrictive(unittest.TestCase):
|
||||
self.assertEqual(gen1.locations[3].item, gen2.locations[3].item)
|
||||
|
||||
def test_can_reserve_advancement_items_for_general_fill(self):
|
||||
"""Test that priority locations fill still satisfies item rules"""
|
||||
multi_world = generate_multi_world()
|
||||
player1 = generate_player_data(
|
||||
multi_world, 1, location_count=5, prog_item_count=5)
|
||||
@@ -655,14 +681,14 @@ class TestDistributeItemsRestrictive(unittest.TestCase):
|
||||
|
||||
location = player1.locations[0]
|
||||
location.progress_type = LocationProgressType.PRIORITY
|
||||
location.item_rule = lambda item: item != items[
|
||||
0] and item != items[1] and item != items[2] and item != items[3]
|
||||
location.item_rule = lambda item: item not in items[:4]
|
||||
|
||||
distribute_items_restrictive(multi_world)
|
||||
|
||||
self.assertEqual(location.item, items[4])
|
||||
|
||||
def test_non_excluded_local_items(self):
|
||||
"""Test that local items get placed locally in a multiworld"""
|
||||
multi_world = generate_multi_world(2)
|
||||
player1 = generate_player_data(
|
||||
multi_world, 1, location_count=5, basic_item_count=5)
|
||||
@@ -683,6 +709,7 @@ class TestDistributeItemsRestrictive(unittest.TestCase):
|
||||
self.assertFalse(item.location.event, False)
|
||||
|
||||
def test_early_items(self) -> None:
|
||||
"""Test that the early items API successfully places items early"""
|
||||
mw = generate_multi_world(2)
|
||||
player1 = generate_player_data(mw, 1, location_count=5, basic_item_count=5)
|
||||
player2 = generate_player_data(mw, 2, location_count=5, basic_item_count=5)
|
||||
@@ -762,21 +789,22 @@ class TestBalanceMultiworldProgression(unittest.TestCase):
|
||||
|
||||
# Sphere 1
|
||||
region = player1.generate_region(player1.menu, 20)
|
||||
items = fillRegion(multi_world, region, [
|
||||
items = fill_region(multi_world, region, [
|
||||
player1.prog_items[0]] + items)
|
||||
|
||||
# Sphere 2
|
||||
region = player1.generate_region(
|
||||
player1.regions[1], 20, lambda state: state.has(player1.prog_items[0].name, player1.id))
|
||||
items = fillRegion(
|
||||
items = fill_region(
|
||||
multi_world, region, [player1.prog_items[1], player2.prog_items[0]] + items)
|
||||
|
||||
# Sphere 3
|
||||
region = player2.generate_region(
|
||||
player2.menu, 20, lambda state: state.has(player2.prog_items[0].name, player2.id))
|
||||
fillRegion(multi_world, region, [player2.prog_items[1]] + items)
|
||||
fill_region(multi_world, region, [player2.prog_items[1]] + items)
|
||||
|
||||
def test_balances_progression(self) -> None:
|
||||
"""Tests that progression balancing moves progression items earlier"""
|
||||
self.multi_world.progression_balancing[self.player1.id].value = 50
|
||||
self.multi_world.progression_balancing[self.player2.id].value = 50
|
||||
|
||||
@@ -789,6 +817,7 @@ class TestBalanceMultiworldProgression(unittest.TestCase):
|
||||
self.player1.regions[1], self.player2.prog_items[0])
|
||||
|
||||
def test_balances_progression_light(self) -> None:
|
||||
"""Test that progression balancing still moves items earlier on minimum value"""
|
||||
self.multi_world.progression_balancing[self.player1.id].value = 1
|
||||
self.multi_world.progression_balancing[self.player2.id].value = 1
|
||||
|
||||
@@ -802,6 +831,7 @@ class TestBalanceMultiworldProgression(unittest.TestCase):
|
||||
self.player1.regions[1], self.player2.prog_items[0])
|
||||
|
||||
def test_balances_progression_heavy(self) -> None:
|
||||
"""Test that progression balancing moves items earlier on maximum value"""
|
||||
self.multi_world.progression_balancing[self.player1.id].value = 99
|
||||
self.multi_world.progression_balancing[self.player2.id].value = 99
|
||||
|
||||
@@ -815,6 +845,7 @@ class TestBalanceMultiworldProgression(unittest.TestCase):
|
||||
self.player1.regions[1], self.player2.prog_items[0])
|
||||
|
||||
def test_skips_balancing_progression(self) -> None:
|
||||
"""Test that progression balancing is skipped when players have it disabled"""
|
||||
self.multi_world.progression_balancing[self.player1.id].value = 0
|
||||
self.multi_world.progression_balancing[self.player2.id].value = 0
|
||||
|
||||
@@ -827,6 +858,7 @@ class TestBalanceMultiworldProgression(unittest.TestCase):
|
||||
self.player1.regions[2], self.player2.prog_items[0])
|
||||
|
||||
def test_ignores_priority_locations(self) -> None:
|
||||
"""Test that progression items on priority locations don't get moved by balancing"""
|
||||
self.multi_world.progression_balancing[self.player1.id].value = 50
|
||||
self.multi_world.progression_balancing[self.player2.id].value = 50
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
from argparse import Namespace
|
||||
from typing import Dict, Optional, Callable
|
||||
|
||||
from BaseClasses import MultiWorld, CollectionState, Region
|
||||
import unittest
|
||||
from typing import Callable, Dict, Optional
|
||||
|
||||
from BaseClasses import CollectionState, MultiWorld, Region
|
||||
|
||||
|
||||
class TestHelpers(unittest.TestCase):
|
||||
@@ -15,7 +14,8 @@ class TestHelpers(unittest.TestCase):
|
||||
self.multiworld.player_name = {1: "Tester"}
|
||||
self.multiworld.set_seed()
|
||||
|
||||
def testRegionHelpers(self) -> None:
|
||||
def test_region_helpers(self) -> None:
|
||||
"""Tests `Region.add_locations()` and `Region.add_exits()` have correct behavior"""
|
||||
regions: Dict[str, str] = {
|
||||
"TestRegion1": "I'm an apple",
|
||||
"TestRegion2": "I'm a banana",
|
||||
@@ -79,4 +79,5 @@ class TestHelpers(unittest.TestCase):
|
||||
current_region.add_exits(reg_exit_set[region])
|
||||
exit_names = {_exit.name for _exit in current_region.exits}
|
||||
for reg_exit in reg_exit_set[region]:
|
||||
self.assertTrue(f"{region} -> {reg_exit}" in exit_names, f"{region} -> {reg_exit} not in {exit_names}")
|
||||
self.assertTrue(f"{region} -> {reg_exit}" in exit_names,
|
||||
f"{region} -> {reg_exit} not in {exit_names}")
|
||||
@@ -15,6 +15,7 @@ class TestIDs(unittest.TestCase):
|
||||
cls.yaml_options = Utils.parse_yaml(f.read())
|
||||
|
||||
def test_utils_in_yaml(self) -> None:
|
||||
"""Tests that the auto generated host.yaml has default settings in it"""
|
||||
for option_key, option_set in Utils.get_default_options().items():
|
||||
with self.subTest(option_key):
|
||||
self.assertIn(option_key, self.yaml_options)
|
||||
@@ -22,6 +23,7 @@ class TestIDs(unittest.TestCase):
|
||||
self.assertIn(sub_option_key, self.yaml_options[option_key])
|
||||
|
||||
def test_yaml_in_utils(self) -> None:
|
||||
"""Tests that the auto generated host.yaml shows up in reference calls"""
|
||||
utils_options = Utils.get_default_options()
|
||||
for option_key, option_set in self.yaml_options.items():
|
||||
with self.subTest(option_key):
|
||||
@@ -3,35 +3,37 @@ from worlds.AutoWorld import AutoWorldRegister
|
||||
|
||||
|
||||
class TestIDs(unittest.TestCase):
|
||||
def testUniqueItems(self):
|
||||
def test_unique_items(self):
|
||||
"""Tests that every game has a unique ID per item in the datapackage"""
|
||||
known_item_ids = set()
|
||||
for gamename, world_type in AutoWorldRegister.world_types.items():
|
||||
current = len(known_item_ids)
|
||||
known_item_ids |= set(world_type.item_id_to_name)
|
||||
self.assertEqual(len(known_item_ids) - len(world_type.item_id_to_name), current)
|
||||
|
||||
def testUniqueLocations(self):
|
||||
def test_unique_locations(self):
|
||||
"""Tests that every game has a unique ID per location in the datapackage"""
|
||||
known_location_ids = set()
|
||||
for gamename, world_type in AutoWorldRegister.world_types.items():
|
||||
current = len(known_location_ids)
|
||||
known_location_ids |= set(world_type.location_id_to_name)
|
||||
self.assertEqual(len(known_location_ids) - len(world_type.location_id_to_name), current)
|
||||
|
||||
def testRangeItems(self):
|
||||
def test_range_items(self):
|
||||
"""There are Javascript clients, which are limited to Number.MAX_SAFE_INTEGER due to 64bit float precision."""
|
||||
for gamename, world_type in AutoWorldRegister.world_types.items():
|
||||
with self.subTest(game=gamename):
|
||||
for item_id in world_type.item_id_to_name:
|
||||
self.assertLess(item_id, 2**53)
|
||||
|
||||
def testRangeLocations(self):
|
||||
def test_range_locations(self):
|
||||
"""There are Javascript clients, which are limited to Number.MAX_SAFE_INTEGER due to 64bit float precision."""
|
||||
for gamename, world_type in AutoWorldRegister.world_types.items():
|
||||
with self.subTest(game=gamename):
|
||||
for location_id in world_type.location_id_to_name:
|
||||
self.assertLess(location_id, 2**53)
|
||||
|
||||
def testReservedItems(self):
|
||||
def test_reserved_items(self):
|
||||
"""negative item IDs are reserved to the special "Archipelago" world."""
|
||||
for gamename, world_type in AutoWorldRegister.world_types.items():
|
||||
with self.subTest(game=gamename):
|
||||
@@ -42,7 +44,7 @@ class TestIDs(unittest.TestCase):
|
||||
for item_id in world_type.item_id_to_name:
|
||||
self.assertGreater(item_id, 0)
|
||||
|
||||
def testReservedLocations(self):
|
||||
def test_reserved_locations(self):
|
||||
"""negative location IDs are reserved to the special "Archipelago" world."""
|
||||
for gamename, world_type in AutoWorldRegister.world_types.items():
|
||||
with self.subTest(game=gamename):
|
||||
@@ -53,12 +55,14 @@ class TestIDs(unittest.TestCase):
|
||||
for location_id in world_type.location_id_to_name:
|
||||
self.assertGreater(location_id, 0)
|
||||
|
||||
def testDuplicateItemIDs(self):
|
||||
def test_duplicate_item_ids(self):
|
||||
"""Test that a game doesn't have item id overlap within its own datapackage"""
|
||||
for gamename, world_type in AutoWorldRegister.world_types.items():
|
||||
with self.subTest(game=gamename):
|
||||
self.assertEqual(len(world_type.item_id_to_name), len(world_type.item_name_to_id))
|
||||
|
||||
def testDuplicateLocationIDs(self):
|
||||
def test_duplicate_location_ids(self):
|
||||
"""Test that a game doesn't have location id overlap within its own datapackage"""
|
||||
for gamename, world_type in AutoWorldRegister.world_types.items():
|
||||
with self.subTest(game=gamename):
|
||||
self.assertEqual(len(world_type.location_id_to_name), len(world_type.location_name_to_id))
|
||||
@@ -1,11 +1,13 @@
|
||||
import unittest
|
||||
from worlds.AutoWorld import AutoWorldRegister
|
||||
|
||||
from Fill import distribute_items_restrictive
|
||||
from NetUtils import encode
|
||||
from worlds.AutoWorld import AutoWorldRegister, call_all
|
||||
from . import setup_solo_multiworld
|
||||
|
||||
|
||||
class TestImplemented(unittest.TestCase):
|
||||
def testCompletionCondition(self):
|
||||
def test_completion_condition(self):
|
||||
"""Ensure a completion condition is set that has requirements."""
|
||||
for game_name, world_type in AutoWorldRegister.world_types.items():
|
||||
if not world_type.hidden and game_name not in {"Sudoku"}:
|
||||
@@ -13,7 +15,7 @@ class TestImplemented(unittest.TestCase):
|
||||
multiworld = setup_solo_multiworld(world_type)
|
||||
self.assertFalse(multiworld.completion_condition[1](multiworld.state))
|
||||
|
||||
def testEntranceParents(self):
|
||||
def test_entrance_parents(self):
|
||||
"""Tests that the parents of created Entrances match the exiting Region."""
|
||||
for game_name, world_type in AutoWorldRegister.world_types.items():
|
||||
if not world_type.hidden:
|
||||
@@ -23,7 +25,7 @@ class TestImplemented(unittest.TestCase):
|
||||
for exit in region.exits:
|
||||
self.assertEqual(exit.parent_region, region)
|
||||
|
||||
def testStageMethods(self):
|
||||
def test_stage_methods(self):
|
||||
"""Tests that worlds don't try to implement certain steps that are only ever called as stage."""
|
||||
for game_name, world_type in AutoWorldRegister.world_types.items():
|
||||
if not world_type.hidden:
|
||||
@@ -31,3 +33,17 @@ class TestImplemented(unittest.TestCase):
|
||||
for method in ("assert_generate",):
|
||||
self.assertFalse(hasattr(world_type, method),
|
||||
f"{method} must be implemented as a @classmethod named stage_{method}.")
|
||||
|
||||
def test_slot_data(self):
|
||||
"""Tests that if a world creates slot data, it's json serializable."""
|
||||
for game_name, world_type in AutoWorldRegister.world_types.items():
|
||||
# has an await for generate_output which isn't being called
|
||||
if game_name in {"Ocarina of Time", "Zillion"}:
|
||||
continue
|
||||
with self.subTest(game_name):
|
||||
multiworld = setup_solo_multiworld(world_type)
|
||||
distribute_items_restrictive(multiworld)
|
||||
call_all(multiworld, "post_fill")
|
||||
for key, data in multiworld.worlds[1].fill_slot_data().items():
|
||||
self.assertIsInstance(key, str, "keys in slot data must be a string")
|
||||
self.assertIsInstance(encode(data), str, f"object {type(data).__name__} not serializable.")
|
||||
@@ -4,7 +4,8 @@ from . import setup_solo_multiworld
|
||||
|
||||
|
||||
class TestBase(unittest.TestCase):
|
||||
def testCreateItem(self):
|
||||
def test_create_item(self):
|
||||
"""Test that a world can successfully create all items in its datapackage"""
|
||||
for game_name, world_type in AutoWorldRegister.world_types.items():
|
||||
proxy_world = world_type(None, 0) # this is identical to MultiServer.py creating worlds
|
||||
for item_name in world_type.item_name_to_id:
|
||||
@@ -12,7 +13,7 @@ class TestBase(unittest.TestCase):
|
||||
item = proxy_world.create_item(item_name)
|
||||
self.assertEqual(item.name, item_name)
|
||||
|
||||
def testItemNameGroupHasValidItem(self):
|
||||
def test_item_name_group_has_valid_item(self):
|
||||
"""Test that all item name groups contain valid items. """
|
||||
# This cannot test for Event names that you may have declared for logic, only sendable Items.
|
||||
# In such a case, you can add your entries to this Exclusion dict. Game Name -> Group Names
|
||||
@@ -33,7 +34,7 @@ class TestBase(unittest.TestCase):
|
||||
for item in items:
|
||||
self.assertIn(item, world_type.item_name_to_id)
|
||||
|
||||
def testItemNameGroupConflict(self):
|
||||
def test_item_name_group_conflict(self):
|
||||
"""Test that all item name groups aren't also item names."""
|
||||
for game_name, world_type in AutoWorldRegister.world_types.items():
|
||||
with self.subTest(game_name, game_name=game_name):
|
||||
@@ -41,7 +42,8 @@ class TestBase(unittest.TestCase):
|
||||
with self.subTest(group_name, group_name=group_name):
|
||||
self.assertNotIn(group_name, world_type.item_name_to_id)
|
||||
|
||||
def testItemCountGreaterEqualLocations(self):
|
||||
def test_item_count_greater_equal_locations(self):
|
||||
"""Test that by the pre_fill step under default settings, each game submits items >= locations"""
|
||||
for game_name, world_type in AutoWorldRegister.world_types.items():
|
||||
with self.subTest("Game", game=game_name):
|
||||
multiworld = setup_solo_multiworld(world_type)
|
||||
@@ -5,7 +5,7 @@ from . import setup_solo_multiworld
|
||||
|
||||
|
||||
class TestBase(unittest.TestCase):
|
||||
def testCreateDuplicateLocations(self):
|
||||
def test_create_duplicate_locations(self):
|
||||
"""Tests that no two Locations share a name or ID."""
|
||||
for game_name, world_type in AutoWorldRegister.world_types.items():
|
||||
multiworld = setup_solo_multiworld(world_type)
|
||||
@@ -20,7 +20,7 @@ class TestBase(unittest.TestCase):
|
||||
self.assertLessEqual(locations.most_common(1)[0][1], 1,
|
||||
f"{world_type.game} has duplicate of location ID {locations.most_common(1)}")
|
||||
|
||||
def testLocationsInDatapackage(self):
|
||||
def test_locations_in_datapackage(self):
|
||||
"""Tests that created locations not filled before fill starts exist in the datapackage."""
|
||||
for game_name, world_type in AutoWorldRegister.world_types.items():
|
||||
with self.subTest("Game", game_name=game_name):
|
||||
@@ -30,7 +30,7 @@ class TestBase(unittest.TestCase):
|
||||
self.assertIn(location.name, world_type.location_name_to_id)
|
||||
self.assertEqual(location.address, world_type.location_name_to_id[location.name])
|
||||
|
||||
def testLocationCreationSteps(self):
|
||||
def test_location_creation_steps(self):
|
||||
"""Tests that Regions and Locations aren't created after `create_items`."""
|
||||
gen_steps = ("generate_early", "create_regions", "create_items")
|
||||
for game_name, world_type in AutoWorldRegister.world_types.items():
|
||||
@@ -60,7 +60,7 @@ class TestBase(unittest.TestCase):
|
||||
self.assertGreaterEqual(location_count, len(multiworld.get_locations()),
|
||||
f"{game_name} modified locations count during pre_fill")
|
||||
|
||||
def testLocationGroup(self):
|
||||
def test_location_group(self):
|
||||
"""Test that all location name groups contain valid locations and don't share names."""
|
||||
for game_name, world_type in AutoWorldRegister.world_types.items():
|
||||
with self.subTest(game_name, game_name=game_name):
|
||||
@@ -3,7 +3,7 @@ from worlds.AutoWorld import AutoWorldRegister
|
||||
|
||||
|
||||
class TestNames(unittest.TestCase):
|
||||
def testItemNamesFormat(self):
|
||||
def test_item_names_format(self):
|
||||
"""Item names must not be all numeric in order to differentiate between ID and name in !hint"""
|
||||
for gamename, world_type in AutoWorldRegister.world_types.items():
|
||||
with self.subTest(game=gamename):
|
||||
@@ -11,7 +11,7 @@ class TestNames(unittest.TestCase):
|
||||
self.assertFalse(item_name.isnumeric(),
|
||||
f"Item name \"{item_name}\" is invalid. It must not be numeric.")
|
||||
|
||||
def testLocationNameFormat(self):
|
||||
def test_location_name_format(self):
|
||||
"""Location names must not be all numeric in order to differentiate between ID and name in !hint_location"""
|
||||
for gamename, world_type in AutoWorldRegister.world_types.items():
|
||||
with self.subTest(game=gamename):
|
||||
@@ -3,7 +3,8 @@ from worlds.AutoWorld import AutoWorldRegister
|
||||
|
||||
|
||||
class TestOptions(unittest.TestCase):
|
||||
def testOptionsHaveDocString(self):
|
||||
def test_options_have_doc_string(self):
|
||||
"""Test that submitted options have their own specified docstring"""
|
||||
for gamename, world_type in AutoWorldRegister.world_types.items():
|
||||
if not world_type.hidden:
|
||||
for option_key, option in world_type.options_dataclass.type_hints.items():
|
||||
@@ -31,7 +31,8 @@ class TestBase(unittest.TestCase):
|
||||
}
|
||||
}
|
||||
|
||||
def testDefaultAllStateCanReachEverything(self):
|
||||
def test_default_all_state_can_reach_everything(self):
|
||||
"""Ensure all state can reach everything and complete the game with the defined options"""
|
||||
for game_name, world_type in AutoWorldRegister.world_types.items():
|
||||
unreachable_regions = self.default_settings_unreachable_regions.get(game_name, set())
|
||||
with self.subTest("Game", game=game_name):
|
||||
@@ -54,7 +55,8 @@ class TestBase(unittest.TestCase):
|
||||
with self.subTest("Completion Condition"):
|
||||
self.assertTrue(world.can_beat_game(state))
|
||||
|
||||
def testDefaultEmptyStateCanReachSomething(self):
|
||||
def test_default_empty_state_can_reach_something(self):
|
||||
"""Ensure empty state can reach at least one location with the defined options"""
|
||||
for game_name, world_type in AutoWorldRegister.world_types.items():
|
||||
with self.subTest("Game", game=game_name):
|
||||
world = setup_solo_multiworld(world_type)
|
||||
@@ -16,7 +16,7 @@ class TestGenerateMain(unittest.TestCase):
|
||||
|
||||
generate_dir = Path(Generate.__file__).parent
|
||||
run_dir = generate_dir / "test" # reproducible cwd that's neither __file__ nor Generate.__file__
|
||||
abs_input_dir = Path(__file__).parent / 'data' / 'OnePlayer'
|
||||
abs_input_dir = Path(__file__).parent / 'data' / 'one_player'
|
||||
rel_input_dir = abs_input_dir.relative_to(run_dir) # directly supplied relative paths are relative to cwd
|
||||
yaml_input_dir = abs_input_dir.relative_to(generate_dir) # yaml paths are relative to user_path
|
||||
|
||||
@@ -19,11 +19,11 @@ class TestDocs(unittest.TestCase):
|
||||
|
||||
cls.client = app.test_client()
|
||||
|
||||
def testCorrectErrorEmptyRequest(self):
|
||||
def test_correct_error_empty_request(self):
|
||||
response = self.client.post("/api/generate")
|
||||
self.assertIn("No options found. Expected file attachment or json weights.", response.text)
|
||||
|
||||
def testGenerationQueued(self):
|
||||
def test_generation_queued(self):
|
||||
options = {
|
||||
"Tester1":
|
||||
{
|
||||
@@ -11,7 +11,7 @@ class TestDocs(unittest.TestCase):
|
||||
def setUpClass(cls) -> None:
|
||||
cls.tutorials_data = WebHost.create_ordered_tutorials_file()
|
||||
|
||||
def testHasTutorial(self):
|
||||
def test_has_tutorial(self):
|
||||
games_with_tutorial = set(entry["gameTitle"] for entry in self.tutorials_data)
|
||||
for game_name, world_type in AutoWorldRegister.world_types.items():
|
||||
if not world_type.hidden:
|
||||
@@ -27,7 +27,7 @@ class TestDocs(unittest.TestCase):
|
||||
self.fail(f"{game_name} has no setup tutorial. "
|
||||
f"Games with Tutorial: {games_with_tutorial}")
|
||||
|
||||
def testHasGameInfo(self):
|
||||
def test_has_game_info(self):
|
||||
for game_name, world_type in AutoWorldRegister.world_types.items():
|
||||
if not world_type.hidden:
|
||||
target_path = Utils.local_path("WebHostLib", "static", "generated", "docs", game_name)
|
||||
@@ -13,7 +13,7 @@ class TestFileGeneration(unittest.TestCase):
|
||||
# should not create the folder *here*
|
||||
cls.incorrect_path = os.path.join(os.path.split(os.path.dirname(__file__))[0], "WebHostLib")
|
||||
|
||||
def testOptions(self):
|
||||
def test_options(self):
|
||||
from WebHostLib.options import create as create_options_files
|
||||
create_options_files()
|
||||
target = os.path.join(self.correct_path, "static", "generated", "configs")
|
||||
@@ -30,7 +30,7 @@ class TestFileGeneration(unittest.TestCase):
|
||||
for value in roll_options({file.name: f.read()})[0].values():
|
||||
self.assertTrue(value is True, f"Default Options for template {file.name} cannot be run.")
|
||||
|
||||
def testTutorial(self):
|
||||
def test_tutorial(self):
|
||||
WebHost.create_ordered_tutorials_file()
|
||||
self.assertTrue(os.path.exists(os.path.join(self.correct_path, "static", "generated", "tutorials.json")))
|
||||
self.assertFalse(os.path.exists(os.path.join(self.incorrect_path, "static", "generated", "tutorials.json")))
|
||||
@@ -149,7 +149,7 @@ def call_stage(multiworld: "MultiWorld", method_name: str, *args: Any) -> None:
|
||||
class WebWorld:
|
||||
"""Webhost integration"""
|
||||
|
||||
settings_page: Union[bool, str] = True
|
||||
options_page: Union[bool, str] = True
|
||||
"""display a settings page. Can be a link to a specific page or external tool."""
|
||||
|
||||
game_info_languages: List[str] = ['en']
|
||||
|
||||
@@ -89,9 +89,6 @@ components: List[Component] = [
|
||||
Component('SNI Client', 'SNIClient',
|
||||
file_identifier=SuffixIdentifier('.apz3', '.apm3', '.apsoe', '.aplttp', '.apsm', '.apsmz3', '.apdkc3',
|
||||
'.apsmw', '.apl2ac')),
|
||||
# BizHawk
|
||||
Component("BizHawk Client", "BizHawkClient", component_type=Type.CLIENT,
|
||||
file_identifier=SuffixIdentifier()),
|
||||
Component('Links Awakening DX Client', 'LinksAwakeningClient',
|
||||
file_identifier=SuffixIdentifier('.apladx')),
|
||||
Component('LttP Adjuster', 'LttPAdjuster'),
|
||||
|
||||
@@ -13,7 +13,6 @@ import typing
|
||||
|
||||
|
||||
BIZHAWK_SOCKET_PORT = 43055
|
||||
EXPECTED_SCRIPT_VERSION = 1
|
||||
|
||||
|
||||
class ConnectionStatus(enum.IntEnum):
|
||||
@@ -22,15 +21,6 @@ class ConnectionStatus(enum.IntEnum):
|
||||
CONNECTED = 3
|
||||
|
||||
|
||||
class BizHawkContext:
|
||||
streams: typing.Optional[typing.Tuple[asyncio.StreamReader, asyncio.StreamWriter]]
|
||||
connection_status: ConnectionStatus
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.streams = None
|
||||
self.connection_status = ConnectionStatus.NOT_CONNECTED
|
||||
|
||||
|
||||
class NotConnectedError(Exception):
|
||||
"""Raised when something tries to make a request to the connector script before a connection has been established"""
|
||||
pass
|
||||
@@ -51,6 +41,50 @@ class SyncError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class BizHawkContext:
|
||||
streams: typing.Optional[typing.Tuple[asyncio.StreamReader, asyncio.StreamWriter]]
|
||||
connection_status: ConnectionStatus
|
||||
_lock: asyncio.Lock
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.streams = None
|
||||
self.connection_status = ConnectionStatus.NOT_CONNECTED
|
||||
self._lock = asyncio.Lock()
|
||||
|
||||
async def _send_message(self, message: str):
|
||||
async with self._lock:
|
||||
if self.streams is None:
|
||||
raise NotConnectedError("You tried to send a request before a connection to BizHawk was made")
|
||||
|
||||
try:
|
||||
reader, writer = self.streams
|
||||
writer.write(message.encode("utf-8") + b"\n")
|
||||
await asyncio.wait_for(writer.drain(), timeout=5)
|
||||
|
||||
res = await asyncio.wait_for(reader.readline(), timeout=5)
|
||||
|
||||
if res == b"":
|
||||
writer.close()
|
||||
self.streams = None
|
||||
self.connection_status = ConnectionStatus.NOT_CONNECTED
|
||||
raise RequestFailedError("Connection closed")
|
||||
|
||||
if self.connection_status == ConnectionStatus.TENTATIVE:
|
||||
self.connection_status = ConnectionStatus.CONNECTED
|
||||
|
||||
return res.decode("utf-8")
|
||||
except asyncio.TimeoutError as exc:
|
||||
writer.close()
|
||||
self.streams = None
|
||||
self.connection_status = ConnectionStatus.NOT_CONNECTED
|
||||
raise RequestFailedError("Connection timed out") from exc
|
||||
except ConnectionResetError as exc:
|
||||
writer.close()
|
||||
self.streams = None
|
||||
self.connection_status = ConnectionStatus.NOT_CONNECTED
|
||||
raise RequestFailedError("Connection reset") from exc
|
||||
|
||||
|
||||
async def connect(ctx: BizHawkContext) -> bool:
|
||||
"""Attempts to establish a connection with the connector script. Returns True if successful."""
|
||||
try:
|
||||
@@ -72,74 +106,14 @@ def disconnect(ctx: BizHawkContext) -> None:
|
||||
|
||||
|
||||
async def get_script_version(ctx: BizHawkContext) -> int:
|
||||
if ctx.streams is None:
|
||||
raise NotConnectedError("You tried to send a request before a connection to BizHawk was made")
|
||||
|
||||
try:
|
||||
reader, writer = ctx.streams
|
||||
writer.write("VERSION".encode("ascii") + b"\n")
|
||||
await asyncio.wait_for(writer.drain(), timeout=5)
|
||||
|
||||
version = await asyncio.wait_for(reader.readline(), timeout=5)
|
||||
|
||||
if version == b"":
|
||||
writer.close()
|
||||
ctx.streams = None
|
||||
ctx.connection_status = ConnectionStatus.NOT_CONNECTED
|
||||
raise RequestFailedError("Connection closed")
|
||||
|
||||
return int(version.decode("ascii"))
|
||||
except asyncio.TimeoutError as exc:
|
||||
writer.close()
|
||||
ctx.streams = None
|
||||
ctx.connection_status = ConnectionStatus.NOT_CONNECTED
|
||||
raise RequestFailedError("Connection timed out") from exc
|
||||
except ConnectionResetError as exc:
|
||||
writer.close()
|
||||
ctx.streams = None
|
||||
ctx.connection_status = ConnectionStatus.NOT_CONNECTED
|
||||
raise RequestFailedError("Connection reset") from exc
|
||||
return int(await ctx._send_message("VERSION"))
|
||||
|
||||
|
||||
async def send_requests(ctx: BizHawkContext, req_list: typing.List[typing.Dict[str, typing.Any]]) -> typing.List[typing.Dict[str, typing.Any]]:
|
||||
"""Sends a list of requests to the BizHawk connector and returns their responses.
|
||||
|
||||
It's likely you want to use the wrapper functions instead of this."""
|
||||
if ctx.streams is None:
|
||||
raise NotConnectedError("You tried to send a request before a connection to BizHawk was made")
|
||||
|
||||
try:
|
||||
reader, writer = ctx.streams
|
||||
writer.write(json.dumps(req_list).encode("utf-8") + b"\n")
|
||||
await asyncio.wait_for(writer.drain(), timeout=5)
|
||||
|
||||
res = await asyncio.wait_for(reader.readline(), timeout=5)
|
||||
|
||||
if res == b"":
|
||||
writer.close()
|
||||
ctx.streams = None
|
||||
ctx.connection_status = ConnectionStatus.NOT_CONNECTED
|
||||
raise RequestFailedError("Connection closed")
|
||||
|
||||
if ctx.connection_status == ConnectionStatus.TENTATIVE:
|
||||
ctx.connection_status = ConnectionStatus.CONNECTED
|
||||
|
||||
ret = json.loads(res.decode("utf-8"))
|
||||
for response in ret:
|
||||
if response["type"] == "ERROR":
|
||||
raise ConnectorError(response["err"])
|
||||
|
||||
return ret
|
||||
except asyncio.TimeoutError as exc:
|
||||
writer.close()
|
||||
ctx.streams = None
|
||||
ctx.connection_status = ConnectionStatus.NOT_CONNECTED
|
||||
raise RequestFailedError("Connection timed out") from exc
|
||||
except ConnectionResetError as exc:
|
||||
writer.close()
|
||||
ctx.streams = None
|
||||
ctx.connection_status = ConnectionStatus.NOT_CONNECTED
|
||||
raise RequestFailedError("Connection reset") from exc
|
||||
return json.loads(await ctx._send_message(json.dumps(req_list)))
|
||||
|
||||
|
||||
async def ping(ctx: BizHawkContext) -> None:
|
||||
|
||||
@@ -16,12 +16,22 @@ else:
|
||||
BizHawkClientContext = object
|
||||
|
||||
|
||||
def launch_client(*args) -> None:
|
||||
from .context import launch
|
||||
launch_subprocess(launch, name="BizHawkClient")
|
||||
|
||||
component = Component("BizHawk Client", "BizHawkClient", component_type=Type.CLIENT, func=launch_client,
|
||||
file_identifier=SuffixIdentifier())
|
||||
components.append(component)
|
||||
|
||||
|
||||
class AutoBizHawkClientRegister(abc.ABCMeta):
|
||||
game_handlers: ClassVar[Dict[Tuple[str, ...], Dict[str, BizHawkClient]]] = {}
|
||||
|
||||
def __new__(cls, name: str, bases: Tuple[type, ...], namespace: Dict[str, Any]) -> AutoBizHawkClientRegister:
|
||||
new_class = super().__new__(cls, name, bases, namespace)
|
||||
|
||||
# Register handler
|
||||
if "system" in namespace:
|
||||
systems = (namespace["system"],) if type(namespace["system"]) is str else tuple(sorted(namespace["system"]))
|
||||
if systems not in AutoBizHawkClientRegister.game_handlers:
|
||||
@@ -30,6 +40,19 @@ class AutoBizHawkClientRegister(abc.ABCMeta):
|
||||
if "game" in namespace:
|
||||
AutoBizHawkClientRegister.game_handlers[systems][namespace["game"]] = new_class()
|
||||
|
||||
# Update launcher component's suffixes
|
||||
if "patch_suffix" in namespace:
|
||||
if namespace["patch_suffix"] is not None:
|
||||
existing_identifier: SuffixIdentifier = component.file_identifier
|
||||
new_suffixes = [*existing_identifier.suffixes]
|
||||
|
||||
if type(namespace["patch_suffix"]) is str:
|
||||
new_suffixes.append(namespace["patch_suffix"])
|
||||
else:
|
||||
new_suffixes.extend(namespace["patch_suffix"])
|
||||
|
||||
component.file_identifier = SuffixIdentifier(*new_suffixes)
|
||||
|
||||
return new_class
|
||||
|
||||
@staticmethod
|
||||
@@ -45,11 +68,14 @@ class AutoBizHawkClientRegister(abc.ABCMeta):
|
||||
|
||||
class BizHawkClient(abc.ABC, metaclass=AutoBizHawkClientRegister):
|
||||
system: ClassVar[Union[str, Tuple[str, ...]]]
|
||||
"""The system that the game this client is for runs on"""
|
||||
"""The system(s) that the game this client is for runs on"""
|
||||
|
||||
game: ClassVar[str]
|
||||
"""The game this client is for"""
|
||||
|
||||
patch_suffix: ClassVar[Optional[Union[str, Tuple[str, ...]]]]
|
||||
"""The file extension(s) this client is meant to open and patch (e.g. ".apz3")"""
|
||||
|
||||
@abc.abstractmethod
|
||||
async def validate_rom(self, ctx: BizHawkClientContext) -> bool:
|
||||
"""Should return whether the currently loaded ROM should be handled by this client. You might read the game name
|
||||
@@ -75,13 +101,3 @@ class BizHawkClient(abc.ABC, metaclass=AutoBizHawkClientRegister):
|
||||
def on_package(self, ctx: BizHawkClientContext, cmd: str, args: dict) -> None:
|
||||
"""For handling packages from the server. Called from `BizHawkClientContext.on_package`."""
|
||||
pass
|
||||
|
||||
|
||||
def launch_client(*args) -> None:
|
||||
from .context import launch
|
||||
launch_subprocess(launch, name="BizHawkClient")
|
||||
|
||||
|
||||
if not any(component.script_name == "BizHawkClient" for component in components):
|
||||
components.append(Component("BizHawk Client", "BizHawkClient", component_type=Type.CLIENT, func=launch_client,
|
||||
file_identifier=SuffixIdentifier()))
|
||||
|
||||
@@ -5,6 +5,7 @@ checking or launching the client, otherwise it will probably cause circular impo
|
||||
|
||||
|
||||
import asyncio
|
||||
import subprocess
|
||||
import traceback
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
@@ -12,8 +13,8 @@ from CommonClient import CommonContext, ClientCommandProcessor, get_base_parser,
|
||||
import Patch
|
||||
import Utils
|
||||
|
||||
from . import BizHawkContext, ConnectionStatus, RequestFailedError, connect, disconnect, get_hash, get_script_version, \
|
||||
get_system, ping
|
||||
from . import BizHawkContext, ConnectionStatus, NotConnectedError, RequestFailedError, connect, disconnect, get_hash, \
|
||||
get_script_version, get_system, ping
|
||||
from .client import BizHawkClient, AutoBizHawkClientRegister
|
||||
|
||||
|
||||
@@ -132,6 +133,8 @@ async def _game_watcher(ctx: BizHawkClientContext):
|
||||
except RequestFailedError as exc:
|
||||
logger.info(f"Lost connection to BizHawk: {exc.args[0]}")
|
||||
continue
|
||||
except NotConnectedError:
|
||||
continue
|
||||
|
||||
# Get slot name and send `Connect`
|
||||
if ctx.server is not None and ctx.username is None:
|
||||
@@ -146,8 +149,24 @@ async def _game_watcher(ctx: BizHawkClientContext):
|
||||
|
||||
|
||||
async def _run_game(rom: str):
|
||||
import webbrowser
|
||||
webbrowser.open(rom)
|
||||
import os
|
||||
auto_start = Utils.get_settings().bizhawkclient_options.rom_start
|
||||
|
||||
if auto_start is True:
|
||||
emuhawk_path = Utils.get_settings().bizhawkclient_options.emuhawk_path
|
||||
subprocess.Popen([emuhawk_path, "--lua=data/lua/connector_bizhawk_generic.lua", os.path.realpath(rom)],
|
||||
cwd=Utils.local_path("."),
|
||||
stdin=subprocess.DEVNULL,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL)
|
||||
elif isinstance(auto_start, str):
|
||||
import shlex
|
||||
|
||||
subprocess.Popen([*shlex.split(auto_start), os.path.realpath(rom)],
|
||||
cwd=Utils.local_path("."),
|
||||
stdin=subprocess.DEVNULL,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL)
|
||||
|
||||
|
||||
async def _patch_and_run_game(patch_file: str):
|
||||
|
||||
@@ -477,8 +477,6 @@ def create_inverted_regions(world, player):
|
||||
create_lw_region(world, player, 'Death Mountain Bunny Descent Area')
|
||||
]
|
||||
|
||||
world.initialize_regions()
|
||||
|
||||
|
||||
def mark_dark_world_regions(world, player):
|
||||
# cross world caves may have some sections marked as both in_light_world, and in_dark_work.
|
||||
|
||||
@@ -535,8 +535,6 @@ def set_up_take_anys(world, player):
|
||||
take_any.shop.add_inventory(0, 'Blue Potion', 0, 0)
|
||||
take_any.shop.add_inventory(1, 'Boss Heart Container', 0, 0, create_location=True)
|
||||
|
||||
world.initialize_regions()
|
||||
|
||||
|
||||
def get_pool_core(world, player: int):
|
||||
shuffle = world.shuffle[player]
|
||||
|
||||
@@ -102,7 +102,7 @@ item_table = {'Bow': ItemData(IC.progression, None, 0x0B, 'You have\nchosen the\
|
||||
'Red Pendant': ItemData(IC.progression, 'Crystal', (0x01, 0x32, 0x60, 0x00, 0x69, 0x03), None, None, None, None, None, None, "the red pendant"),
|
||||
'Triforce': ItemData(IC.progression, None, 0x6A, '\n YOU WIN!', 'and the triforce', 'victorious kid', 'victory for sale', 'fungus for the win', 'greedy boy wins game again', 'the Triforce'),
|
||||
'Power Star': ItemData(IC.progression, None, 0x6B, 'a small victory', 'and the power star', 'star-struck kid', 'star for sale', 'see stars with shroom', 'mario powers up again', 'a Power Star'),
|
||||
'Triforce Piece': ItemData(IC.progression, None, 0x6C, 'a small victory', 'and the thirdforce', 'triangular kid', 'triangle for sale', 'fungus for triangle', 'wise boy has triangle again', 'a Triforce Piece'),
|
||||
'Triforce Piece': ItemData(IC.progression_skip_balancing, None, 0x6C, 'a small victory', 'and the thirdforce', 'triangular kid', 'triangle for sale', 'fungus for triangle', 'wise boy has triangle again', 'a Triforce Piece'),
|
||||
'Crystal 1': ItemData(IC.progression, 'Crystal', (0x02, 0x34, 0x64, 0x40, 0x7F, 0x06), None, None, None, None, None, None, "a blue crystal"),
|
||||
'Crystal 2': ItemData(IC.progression, 'Crystal', (0x10, 0x34, 0x64, 0x40, 0x79, 0x06), None, None, None, None, None, None, "a blue crystal"),
|
||||
'Crystal 3': ItemData(IC.progression, 'Crystal', (0x40, 0x34, 0x64, 0x40, 0x6C, 0x06), None, None, None, None, None, None, "a blue crystal"),
|
||||
|
||||
@@ -382,8 +382,6 @@ def create_regions(world, player):
|
||||
create_dw_region(world, player, 'Dark Death Mountain Bunny Descent Area')
|
||||
]
|
||||
|
||||
world.initialize_regions()
|
||||
|
||||
|
||||
def create_lw_region(world: MultiWorld, player: int, name: str, locations=None, exits=None):
|
||||
return _create_region(world, player, name, LTTPRegionType.LightWorld, 'Light World', locations, exits)
|
||||
|
||||
@@ -67,7 +67,7 @@ Wenn du eine Option nicht gewählt haben möchtest, setze ihren Wert einfach auf
|
||||
|
||||
### Überprüfung deiner YAML-Datei
|
||||
|
||||
Wenn man sichergehen will, ob die YAML-Datei funktioniert, kann man dies bei der [YAML Validator](/mysterycheck) Seite
|
||||
Wenn man sichergehen will, ob die YAML-Datei funktioniert, kann man dies bei der [YAML Validator](/check) Seite
|
||||
tun.
|
||||
|
||||
## ein Einzelspielerspiel erstellen
|
||||
|
||||
@@ -82,7 +82,7 @@ debe tener al menos un valor mayor que cero, si no la generación fallará.
|
||||
### Verificando tu archivo YAML
|
||||
|
||||
Si quieres validar que tu fichero YAML para asegurarte que funciona correctamente, puedes hacerlo en la pagina
|
||||
[YAML Validator](/mysterycheck).
|
||||
[YAML Validator](/check).
|
||||
|
||||
## Generar una partida para un jugador
|
||||
|
||||
|
||||
@@ -83,7 +83,7 @@ chaque paramètre il faut au moins une option qui soit paramétrée sur un nombr
|
||||
### Vérifier son fichier YAML
|
||||
|
||||
Si vous voulez valider votre fichier YAML pour être sûr qu'il fonctionne, vous pouvez le vérifier sur la page du
|
||||
[Validateur de YAML](/mysterycheck).
|
||||
[Validateur de YAML](/check).
|
||||
|
||||
## Générer une partie pour un joueur
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ from ..AutoWorld import WebWorld, World
|
||||
|
||||
|
||||
class Bk_SudokuWebWorld(WebWorld):
|
||||
settings_page = "games/Sudoku/info/en"
|
||||
options_page = "games/Sudoku/info/en"
|
||||
theme = 'partyTime'
|
||||
tutorials = [
|
||||
Tutorial(
|
||||
|
||||
@@ -23,13 +23,13 @@ def create_regions(world: MultiWorld, player: int):
|
||||
|
||||
entrance_map = {
|
||||
"Level 1": lambda state:
|
||||
state.has("Booster Bumper", player, 2) and state.has("Treasure Bumper", player, 9),
|
||||
state.has("Booster Bumper", player, 1) and state.has("Treasure Bumper", player, 8),
|
||||
"Level 2": lambda state:
|
||||
state.has("Booster Bumper", player, 3) and state.has("Treasure Bumper", player, 17),
|
||||
state.has("Booster Bumper", player, 2) and state.has("Treasure Bumper", player, 16),
|
||||
"Level 3": lambda state:
|
||||
state.has("Booster Bumper", player, 4) and state.has("Treasure Bumper", player, 25),
|
||||
state.has("Booster Bumper", player, 3) and state.has("Treasure Bumper", player, 24),
|
||||
"Level 4": lambda state:
|
||||
state.has("Booster Bumper", player, 5) and state.has("Treasure Bumper", player, 33)
|
||||
state.has("Booster Bumper", player, 5) and state.has("Treasure Bumper", player, 32)
|
||||
}
|
||||
|
||||
for x, region_name in enumerate(region_map):
|
||||
|
||||
@@ -108,7 +108,7 @@ class BumpStikWorld(World):
|
||||
item_pool += self._create_item_in_quantities(
|
||||
name, frequencies[i])
|
||||
|
||||
item_delta = len(location_table) - len(item_pool) - 1
|
||||
item_delta = len(location_table) - len(item_pool)
|
||||
if item_delta > 0:
|
||||
item_pool += self._create_item_in_quantities(
|
||||
"Score Bonus", item_delta)
|
||||
@@ -116,13 +116,16 @@ class BumpStikWorld(World):
|
||||
self.multiworld.itempool += item_pool
|
||||
|
||||
def set_rules(self):
|
||||
forbid_item(self.multiworld.get_location("Bonus Booster 5", self.player),
|
||||
"Booster Bumper", self.player)
|
||||
|
||||
def generate_basic(self):
|
||||
self.multiworld.get_location("Level 5 - Cleared all Hazards", self.player).place_locked_item(
|
||||
self.create_item(self.get_filler_item_name()))
|
||||
|
||||
for x in range(1, 32):
|
||||
self.multiworld.get_location(f"Treasure Bumper {x + 1}", self.player).access_rule = \
|
||||
lambda state, x = x: state.has("Treasure Bumper", self.player, x)
|
||||
for x in range(1, 5):
|
||||
self.multiworld.get_location(f"Bonus Booster {x + 1}", self.player).access_rule = \
|
||||
lambda state, x = x: state.has("Booster Bumper", self.player, x)
|
||||
self.multiworld.get_location("Level 5 - Cleared all Hazards", self.player).access_rule = \
|
||||
lambda state: state.has("Hazard Bumper", self.player, 25)
|
||||
|
||||
self.multiworld.completion_condition[self.player] = \
|
||||
lambda state: state.has("Booster Bumper", self.player, 5) and \
|
||||
state.has("Treasure Bumper", self.player, 32)
|
||||
|
||||
|
||||
39
worlds/bumpstik/test/TestLogic.py
Normal file
@@ -0,0 +1,39 @@
|
||||
from . import BumpStikTestBase
|
||||
|
||||
|
||||
class TestRuleLogic(BumpStikTestBase):
|
||||
def testLogic(self):
|
||||
for x in range(1, 33):
|
||||
if x == 32:
|
||||
self.assertFalse(self.can_reach_location("Level 5 - Cleared all Hazards"))
|
||||
|
||||
self.collect(self.get_item_by_name("Treasure Bumper"))
|
||||
if x % 8 == 0:
|
||||
bb_count = round(x / 8)
|
||||
|
||||
if bb_count < 4:
|
||||
self.assertFalse(self.can_reach_location(f"Treasure Bumper {x + 1}"))
|
||||
elif bb_count == 4:
|
||||
bb_count += 1
|
||||
|
||||
for y in range(self.count("Booster Bumper"), bb_count):
|
||||
self.assertTrue(self.can_reach_location(f"Bonus Booster {y + 1}"),
|
||||
f"BB {y + 1} check not reachable with {self.count('Booster Bumper')} BBs")
|
||||
if y < 4:
|
||||
self.assertFalse(self.can_reach_location(f"Bonus Booster {y + 2}"),
|
||||
f"BB {y + 2} check reachable with {self.count('Treasure Bumper')} TBs")
|
||||
self.collect(self.get_item_by_name("Booster Bumper"))
|
||||
|
||||
if x < 31:
|
||||
self.assertFalse(self.can_reach_location(f"Treasure Bumper {x + 2}"))
|
||||
elif x == 31:
|
||||
self.assertFalse(self.can_reach_location("Level 5 - 50,000+ Total Points"))
|
||||
|
||||
if x < 32:
|
||||
self.assertTrue(self.can_reach_location(f"Treasure Bumper {x + 1}"),
|
||||
f"TB {x + 1} check not reachable with {self.count('Treasure Bumper')} TBs")
|
||||
elif x == 32:
|
||||
self.assertTrue(self.can_reach_location("Level 5 - 50,000+ Total Points"))
|
||||
self.assertFalse(self.can_reach_location("Level 5 - Cleared all Hazards"))
|
||||
self.collect(self.get_items_by_name("Hazard Bumper"))
|
||||
self.assertTrue(self.can_reach_location("Level 5 - Cleared all Hazards"))
|
||||
5
worlds/bumpstik/test/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from test.TestBase import WorldTestBase
|
||||
|
||||
|
||||
class BumpStikTestBase(WorldTestBase):
|
||||
game = "Bumper Stickers"
|
||||
@@ -50,7 +50,7 @@ them. Player settings page: [Donkey Kong Country 3 Player Settings Page](/games/
|
||||
### Verifying your config file
|
||||
|
||||
If you would like to validate your config file to make sure it works, you may do so on the YAML Validator page. YAML
|
||||
validator page: [YAML Validation page](/mysterycheck)
|
||||
validator page: [YAML Validation page](/check)
|
||||
|
||||
## Generating a Single-Player Game
|
||||
|
||||
|
||||
@@ -11,6 +11,8 @@ from . import Options, data
|
||||
|
||||
class DLCQuestItem(Item):
|
||||
game: str = "DLCQuest"
|
||||
coins: int = 0
|
||||
coin_suffix: str = ""
|
||||
|
||||
|
||||
offset = 120_000
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import math
|
||||
from typing import List
|
||||
|
||||
from BaseClasses import Entrance, MultiWorld, Region
|
||||
from . import Options
|
||||
@@ -9,318 +10,181 @@ DLCQuestRegion = ["Movement Pack", "Behind Tree", "Psychological Warfare", "Doub
|
||||
"Double Jump Behind the Tree", "The Forest", "Final Room"]
|
||||
|
||||
|
||||
def add_coin_freemium(region: Region, Coin: int, player: int):
|
||||
number_coin = f"{Coin} coins freemium"
|
||||
location_coin = f"{region.name} coins freemium"
|
||||
def add_coin_lfod(region: Region, coin: int, player: int):
|
||||
add_coin(region, coin, player, " coins freemium")
|
||||
|
||||
|
||||
def add_coin_dlcquest(region: Region, coin: int, player: int):
|
||||
add_coin(region, coin, player, " coins")
|
||||
|
||||
|
||||
def add_coin(region: Region, coin: int, player: int, suffix: str):
|
||||
number_coin = f"{coin}{suffix}"
|
||||
location_coin = f"{region.name}{suffix}"
|
||||
location = DLCQuestLocation(player, location_coin, None, region)
|
||||
region.locations.append(location)
|
||||
location.place_locked_item(create_event(player, number_coin))
|
||||
event = create_event(player, number_coin)
|
||||
event.coins = coin
|
||||
event.coin_suffix = suffix
|
||||
location.place_locked_item(event)
|
||||
|
||||
|
||||
def add_coin_dlcquest(region: Region, Coin: int, player: int):
|
||||
number_coin = f"{Coin} coins"
|
||||
location_coin = f"{region.name} coins"
|
||||
location = DLCQuestLocation(player, location_coin, None, region)
|
||||
region.locations.append(location)
|
||||
location.place_locked_item(create_event(player, number_coin))
|
||||
def create_regions(multiworld: MultiWorld, player: int, world_options: Options.DLCQuestOptions):
|
||||
region_menu = Region("Menu", player, multiworld)
|
||||
has_campaign_basic = world_options.campaign == Options.Campaign.option_basic or world_options.campaign == Options.Campaign.option_both
|
||||
has_campaign_lfod = world_options.campaign == Options.Campaign.option_live_freemium_or_die or world_options.campaign == Options.Campaign.option_both
|
||||
has_coinsanity = world_options.coinsanity == Options.CoinSanity.option_coin
|
||||
coin_bundle_size = world_options.coinbundlequantity.value
|
||||
has_item_shuffle = world_options.item_shuffle == Options.ItemShuffle.option_shuffled
|
||||
|
||||
multiworld.regions.append(region_menu)
|
||||
|
||||
create_regions_basic_campaign(has_campaign_basic, region_menu, has_item_shuffle, has_coinsanity, coin_bundle_size, player, multiworld)
|
||||
|
||||
create_regions_lfod_campaign(coin_bundle_size, has_campaign_lfod, has_coinsanity, has_item_shuffle, multiworld, player, region_menu)
|
||||
|
||||
|
||||
def create_regions(world: MultiWorld, player: int, World_Options: Options.DLCQuestOptions):
|
||||
Regmenu = Region("Menu", player, world)
|
||||
if (World_Options.campaign == Options.Campaign.option_basic or World_Options.campaign
|
||||
== Options.Campaign.option_both):
|
||||
Regmenu.exits += [Entrance(player, "DLC Quest Basic", Regmenu)]
|
||||
if (World_Options.campaign == Options.Campaign.option_live_freemium_or_die or World_Options.campaign
|
||||
== Options.Campaign.option_both):
|
||||
Regmenu.exits += [Entrance(player, "Live Freemium or Die", Regmenu)]
|
||||
world.regions.append(Regmenu)
|
||||
def create_regions_basic_campaign(has_campaign_basic: bool, region_menu: Region, has_item_shuffle: bool, has_coinsanity: bool,
|
||||
coin_bundle_size: int, player: int, world: MultiWorld):
|
||||
if not has_campaign_basic:
|
||||
return
|
||||
|
||||
if (World_Options.campaign == Options.Campaign.option_basic or World_Options.campaign
|
||||
== Options.Campaign.option_both):
|
||||
region_menu.exits += [Entrance(player, "DLC Quest Basic", region_menu)]
|
||||
locations_move_right = ["Movement Pack", "Animation Pack", "Audio Pack", "Pause Menu Pack"]
|
||||
region_move_right = create_region_and_locations_basic("Move Right", locations_move_right, ["Moving"], player, world, 4)
|
||||
create_coinsanity_locations_dlc_quest(has_coinsanity, coin_bundle_size, player, region_move_right)
|
||||
locations_movement_pack = ["Time is Money Pack", "Psychological Warfare Pack", "Armor for your Horse Pack", "Shepherd Sheep"]
|
||||
locations_movement_pack += conditional_location(has_item_shuffle, "Sword")
|
||||
create_region_and_locations_basic("Movement Pack", locations_movement_pack, ["Tree", "Cloud"], player, world, 46)
|
||||
locations_behind_tree = ["Double Jump Pack", "Map Pack", "Between Trees Sheep", "Hole in the Wall Sheep"] + conditional_location(has_item_shuffle, "Gun")
|
||||
create_region_and_locations_basic("Behind Tree", locations_behind_tree, ["Behind Tree Double Jump", "Forest Entrance"], player, world, 60)
|
||||
create_region_and_locations_basic("Psychological Warfare", ["West Cave Sheep"], ["Cloud Double Jump"], player, world, 100)
|
||||
locations_double_jump_left = ["Pet Pack", "Top Hat Pack", "North West Alcove Sheep"]
|
||||
create_region_and_locations_basic("Double Jump Total Left", locations_double_jump_left, ["Cave Tree", "Cave Roof"], player, world, 50)
|
||||
create_region_and_locations_basic("Double Jump Total Left Cave", ["Top Hat Sheep"], [], player, world, 9)
|
||||
create_region_and_locations_basic("Double Jump Total Left Roof", ["North West Ceiling Sheep"], [], player, world, 10)
|
||||
locations_double_jump_left_ceiling = ["Sexy Outfits Pack", "Double Jump Alcove Sheep", "Sexy Outfits Sheep"]
|
||||
create_region_and_locations_basic("Double Jump Behind Tree", locations_double_jump_left_ceiling, ["True Double Jump"], player, world, 89)
|
||||
create_region_and_locations_basic("True Double Jump Behind Tree", ["Double Jump Floating Sheep", "Cutscene Sheep"], [], player, world, 7)
|
||||
create_region_and_locations_basic("The Forest", ["Gun Pack", "Night Map Pack"], ["Behind Ogre", "Forest Double Jump"], player, world, 171)
|
||||
create_region_and_locations_basic("The Forest with double Jump", ["The Zombie Pack", "Forest Low Sheep"], ["Forest True Double Jump"], player, world, 76)
|
||||
create_region_and_locations_basic("The Forest with double Jump Part 2", ["Forest High Sheep"], [], player, world, 203)
|
||||
region_final_boss_room = create_region_and_locations_basic("The Final Boss Room", ["Finish the Fight Pack"], [], player, world)
|
||||
|
||||
Regmoveright = Region("Move Right", player, world, "Start of the basic game")
|
||||
Locmoveright_name = ["Movement Pack", "Animation Pack", "Audio Pack", "Pause Menu Pack"]
|
||||
Regmoveright.exits = [Entrance(player, "Moving", Regmoveright)]
|
||||
Regmoveright.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regmoveright) for
|
||||
loc_name in Locmoveright_name]
|
||||
add_coin_dlcquest(Regmoveright, 4, player)
|
||||
if World_Options.coinsanity == Options.CoinSanity.option_coin:
|
||||
coin_bundle_needed = math.floor(825 / World_Options.coinbundlequantity)
|
||||
for i in range(coin_bundle_needed):
|
||||
item_coin = f"DLC Quest: {World_Options.coinbundlequantity * (i + 1)} Coin"
|
||||
Regmoveright.locations += [
|
||||
DLCQuestLocation(player, item_coin, location_table[item_coin], Regmoveright)]
|
||||
if 825 % World_Options.coinbundlequantity != 0:
|
||||
Regmoveright.locations += [
|
||||
DLCQuestLocation(player, "DLC Quest: 825 Coin", location_table["DLC Quest: 825 Coin"],
|
||||
Regmoveright)]
|
||||
world.regions.append(Regmoveright)
|
||||
create_victory_event(region_final_boss_room, "Winning Basic", "Victory Basic", player)
|
||||
|
||||
Regmovpack = Region("Movement Pack", player, world)
|
||||
Locmovpack_name = ["Time is Money Pack", "Psychological Warfare Pack", "Armor for your Horse Pack",
|
||||
"Shepherd Sheep"]
|
||||
if World_Options.item_shuffle == Options.ItemShuffle.option_shuffled:
|
||||
Locmovpack_name += ["Sword"]
|
||||
Regmovpack.exits = [Entrance(player, "Tree", Regmovpack), Entrance(player, "Cloud", Regmovpack)]
|
||||
Regmovpack.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regmovpack) for loc_name
|
||||
in Locmovpack_name]
|
||||
add_coin_dlcquest(Regmovpack, 46, player)
|
||||
world.regions.append(Regmovpack)
|
||||
connect_entrances_basic(player, world)
|
||||
|
||||
Regbtree = Region("Behind Tree", player, world)
|
||||
Locbtree_name = ["Double Jump Pack", "Map Pack", "Between Trees Sheep", "Hole in the Wall Sheep"]
|
||||
if World_Options.item_shuffle == Options.ItemShuffle.option_shuffled:
|
||||
Locbtree_name += ["Gun"]
|
||||
Regbtree.exits = [Entrance(player, "Behind Tree Double Jump", Regbtree),
|
||||
Entrance(player, "Forest Entrance", Regbtree)]
|
||||
Regbtree.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regbtree) for loc_name in
|
||||
Locbtree_name]
|
||||
add_coin_dlcquest(Regbtree, 60, player)
|
||||
world.regions.append(Regbtree)
|
||||
|
||||
Regpsywarfare = Region("Psychological Warfare", player, world)
|
||||
Locpsywarfare_name = ["West Cave Sheep"]
|
||||
Regpsywarfare.exits = [Entrance(player, "Cloud Double Jump", Regpsywarfare)]
|
||||
Regpsywarfare.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regpsywarfare) for
|
||||
loc_name in Locpsywarfare_name]
|
||||
add_coin_dlcquest(Regpsywarfare, 100, player)
|
||||
world.regions.append(Regpsywarfare)
|
||||
def create_regions_lfod_campaign(coin_bundle_size, has_campaign_lfod, has_coinsanity, has_item_shuffle, multiworld, player, region_menu):
|
||||
if not has_campaign_lfod:
|
||||
return
|
||||
|
||||
Regdoubleleft = Region("Double Jump Total Left", player, world)
|
||||
Locdoubleleft_name = ["Pet Pack", "Top Hat Pack", "North West Alcove Sheep"]
|
||||
Regdoubleleft.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regdoubleleft) for
|
||||
loc_name in
|
||||
Locdoubleleft_name]
|
||||
Regdoubleleft.exits = [Entrance(player, "Cave Tree", Regdoubleleft),
|
||||
Entrance(player, "Cave Roof", Regdoubleleft)]
|
||||
add_coin_dlcquest(Regdoubleleft, 50, player)
|
||||
world.regions.append(Regdoubleleft)
|
||||
region_menu.exits += [Entrance(player, "Live Freemium or Die", region_menu)]
|
||||
locations_lfod_start = ["Particles Pack", "Day One Patch Pack", "Checkpoint Pack", "Incredibly Important Pack",
|
||||
"Nice Try", "Story is Important", "I Get That Reference!"] + conditional_location(has_item_shuffle, "Wooden Sword")
|
||||
region_lfod_start = create_region_and_locations_lfod("Freemium Start", locations_lfod_start, ["Vines"], player, multiworld, 50)
|
||||
create_coinsanity_locations_lfod(has_coinsanity, coin_bundle_size, player, region_lfod_start)
|
||||
locations_behind_vines = ["Wall Jump Pack", "Health Bar Pack", "Parallax Pack"] + conditional_location(has_item_shuffle, "Pickaxe")
|
||||
create_region_and_locations_lfod("Behind the Vines", locations_behind_vines, ["Wall Jump Entrance"], player, multiworld, 95)
|
||||
locations_wall_jump = ["Harmless Plants Pack", "Death of Comedy Pack", "Canadian Dialog Pack", "DLC NPC Pack"]
|
||||
create_region_and_locations_lfod("Wall Jump", locations_wall_jump, ["Harmless Plants", "Pickaxe Hard Cave"], player, multiworld, 150)
|
||||
create_region_and_locations_lfod("Fake Ending", ["Cut Content Pack", "Name Change Pack"], ["Name Change Entrance", "Cut Content Entrance"], player,
|
||||
multiworld)
|
||||
create_region_and_locations_lfod("Hard Cave", [], ["Hard Cave Wall Jump"], player, multiworld, 20)
|
||||
create_region_and_locations_lfod("Hard Cave Wall Jump", ["Increased HP Pack"], [], player, multiworld, 130)
|
||||
create_region_and_locations_lfod("Cut Content", conditional_location(has_item_shuffle, "Humble Indie Bindle"), [], player, multiworld, 200)
|
||||
create_region_and_locations_lfod("Name Change", conditional_location(has_item_shuffle, "Box of Various Supplies"), ["Behind Rocks"], player, multiworld)
|
||||
create_region_and_locations_lfod("Top Right", ["Season Pass", "High Definition Next Gen Pack"], ["Blizzard"], player, multiworld, 90)
|
||||
create_region_and_locations_lfod("Season", ["Remove Ads Pack", "Not Exactly Noble"], ["Boss Door"], player, multiworld, 154)
|
||||
region_final_boss = create_region_and_locations_lfod("Final Boss", ["Big Sword Pack", "Really Big Sword Pack", "Unfathomable Sword Pack"], [], player, multiworld)
|
||||
|
||||
Regdoubleleftcave = Region("Double Jump Total Left Cave", player, world)
|
||||
Locdoubleleftcave_name = ["Top Hat Sheep"]
|
||||
Regdoubleleftcave.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regdoubleleftcave)
|
||||
for loc_name in Locdoubleleftcave_name]
|
||||
add_coin_dlcquest(Regdoubleleftcave, 9, player)
|
||||
world.regions.append(Regdoubleleftcave)
|
||||
create_victory_event(region_final_boss, "Winning Freemium", "Victory Freemium", player)
|
||||
|
||||
Regdoubleleftroof = Region("Double Jump Total Left Roof", player, world)
|
||||
Locdoubleleftroof_name = ["North West Ceiling Sheep"]
|
||||
Regdoubleleftroof.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regdoubleleftroof)
|
||||
for loc_name in Locdoubleleftroof_name]
|
||||
add_coin_dlcquest(Regdoubleleftroof, 10, player)
|
||||
world.regions.append(Regdoubleleftroof)
|
||||
connect_entrances_lfod(multiworld, player)
|
||||
|
||||
Regdoubletree = Region("Double Jump Behind Tree", player, world)
|
||||
Locdoubletree_name = ["Sexy Outfits Pack", "Double Jump Alcove Sheep", "Sexy Outfits Sheep"]
|
||||
Regdoubletree.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regdoubletree) for
|
||||
loc_name in
|
||||
Locdoubletree_name]
|
||||
Regdoubletree.exits = [Entrance(player, "True Double Jump", Regdoubletree)]
|
||||
add_coin_dlcquest(Regdoubletree, 89, player)
|
||||
world.regions.append(Regdoubletree)
|
||||
|
||||
Regtruedoublejump = Region("True Double Jump Behind Tree", player, world)
|
||||
Loctruedoublejump_name = ["Double Jump Floating Sheep", "Cutscene Sheep"]
|
||||
Regtruedoublejump.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regtruedoublejump)
|
||||
for loc_name in Loctruedoublejump_name]
|
||||
add_coin_dlcquest(Regtruedoublejump, 7, player)
|
||||
world.regions.append(Regtruedoublejump)
|
||||
def conditional_location(condition: bool, location: str) -> List[str]:
|
||||
return conditional_locations(condition, [location])
|
||||
|
||||
Regforest = Region("The Forest", player, world)
|
||||
Locforest_name = ["Gun Pack", "Night Map Pack"]
|
||||
Regforest.exits = [Entrance(player, "Behind Ogre", Regforest),
|
||||
Entrance(player, "Forest Double Jump", Regforest)]
|
||||
Regforest.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regforest) for loc_name in
|
||||
Locforest_name]
|
||||
add_coin_dlcquest(Regforest, 171, player)
|
||||
world.regions.append(Regforest)
|
||||
|
||||
Regforestdoublejump = Region("The Forest whit double Jump", player, world)
|
||||
Locforestdoublejump_name = ["The Zombie Pack", "Forest Low Sheep"]
|
||||
Regforestdoublejump.exits = [Entrance(player, "Forest True Double Jump", Regforestdoublejump)]
|
||||
Regforestdoublejump.locations += [
|
||||
DLCQuestLocation(player, loc_name, location_table[loc_name], Regforestdoublejump) for loc_name in
|
||||
Locforestdoublejump_name]
|
||||
add_coin_dlcquest(Regforestdoublejump, 76, player)
|
||||
world.regions.append(Regforestdoublejump)
|
||||
def conditional_locations(condition: bool, locations: List[str]) -> List[str]:
|
||||
return locations if condition else []
|
||||
|
||||
Regforesttruedoublejump = Region("The Forest whit double Jump Part 2", player, world)
|
||||
Locforesttruedoublejump_name = ["Forest High Sheep"]
|
||||
Regforesttruedoublejump.locations += [
|
||||
DLCQuestLocation(player, loc_name, location_table[loc_name], Regforesttruedoublejump)
|
||||
for loc_name in Locforesttruedoublejump_name]
|
||||
add_coin_dlcquest(Regforesttruedoublejump, 203, player)
|
||||
world.regions.append(Regforesttruedoublejump)
|
||||
|
||||
Regfinalroom = Region("The Final Boss Room", player, world)
|
||||
Locfinalroom_name = ["Finish the Fight Pack"]
|
||||
Regfinalroom.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regfinalroom) for
|
||||
loc_name in
|
||||
Locfinalroom_name]
|
||||
world.regions.append(Regfinalroom)
|
||||
def create_region_and_locations_basic(region_name: str, locations: List[str], exits: List[str], player: int, multiworld: MultiWorld,
|
||||
number_coins: int = 0) -> Region:
|
||||
return create_region_and_locations(region_name, locations, exits, player, multiworld, number_coins, 0)
|
||||
|
||||
loc_win = DLCQuestLocation(player, "Winning Basic", None, world.get_region("The Final Boss Room", player))
|
||||
world.get_region("The Final Boss Room", player).locations.append(loc_win)
|
||||
loc_win.place_locked_item(create_event(player, "Victory Basic"))
|
||||
|
||||
world.get_entrance("DLC Quest Basic", player).connect(world.get_region("Move Right", player))
|
||||
def create_region_and_locations_lfod(region_name: str, locations: List[str], exits: List[str], player: int, multiworld: MultiWorld,
|
||||
number_coins: int = 0) -> Region:
|
||||
return create_region_and_locations(region_name, locations, exits, player, multiworld, 0, number_coins)
|
||||
|
||||
world.get_entrance("Moving", player).connect(world.get_region("Movement Pack", player))
|
||||
|
||||
world.get_entrance("Tree", player).connect(world.get_region("Behind Tree", player))
|
||||
def create_region_and_locations(region_name: str, locations: List[str], exits: List[str], player: int, multiworld: MultiWorld,
|
||||
number_coins_basic: int, number_coins_lfod: int) -> Region:
|
||||
region = Region(region_name, player, multiworld)
|
||||
region.exits = [Entrance(player, exit_name, region) for exit_name in exits]
|
||||
region.locations += [DLCQuestLocation(player, name, location_table[name], region) for name in locations]
|
||||
if number_coins_basic > 0:
|
||||
add_coin_dlcquest(region, number_coins_basic, player)
|
||||
if number_coins_lfod > 0:
|
||||
add_coin_lfod(region, number_coins_lfod, player)
|
||||
multiworld.regions.append(region)
|
||||
return region
|
||||
|
||||
world.get_entrance("Cloud", player).connect(world.get_region("Psychological Warfare", player))
|
||||
|
||||
world.get_entrance("Cloud Double Jump", player).connect(world.get_region("Double Jump Total Left", player))
|
||||
def create_victory_event(region_victory: Region, event_name: str, item_name: str, player: int):
|
||||
location_victory = DLCQuestLocation(player, event_name, None, region_victory)
|
||||
region_victory.locations.append(location_victory)
|
||||
location_victory.place_locked_item(create_event(player, item_name))
|
||||
|
||||
world.get_entrance("Cave Tree", player).connect(world.get_region("Double Jump Total Left Cave", player))
|
||||
|
||||
world.get_entrance("Cave Roof", player).connect(world.get_region("Double Jump Total Left Roof", player))
|
||||
def connect_entrances_basic(player, world):
|
||||
world.get_entrance("DLC Quest Basic", player).connect(world.get_region("Move Right", player))
|
||||
world.get_entrance("Moving", player).connect(world.get_region("Movement Pack", player))
|
||||
world.get_entrance("Tree", player).connect(world.get_region("Behind Tree", player))
|
||||
world.get_entrance("Cloud", player).connect(world.get_region("Psychological Warfare", player))
|
||||
world.get_entrance("Cloud Double Jump", player).connect(world.get_region("Double Jump Total Left", player))
|
||||
world.get_entrance("Cave Tree", player).connect(world.get_region("Double Jump Total Left Cave", player))
|
||||
world.get_entrance("Cave Roof", player).connect(world.get_region("Double Jump Total Left Roof", player))
|
||||
world.get_entrance("Forest Entrance", player).connect(world.get_region("The Forest", player))
|
||||
world.get_entrance("Behind Tree Double Jump", player).connect(world.get_region("Double Jump Behind Tree", player))
|
||||
world.get_entrance("Behind Ogre", player).connect(world.get_region("The Final Boss Room", player))
|
||||
world.get_entrance("Forest Double Jump", player).connect(world.get_region("The Forest with double Jump", player))
|
||||
world.get_entrance("Forest True Double Jump", player).connect(world.get_region("The Forest with double Jump Part 2", player))
|
||||
world.get_entrance("True Double Jump", player).connect(world.get_region("True Double Jump Behind Tree", player))
|
||||
|
||||
world.get_entrance("Forest Entrance", player).connect(world.get_region("The Forest", player))
|
||||
|
||||
world.get_entrance("Behind Tree Double Jump", player).connect(
|
||||
world.get_region("Double Jump Behind Tree", player))
|
||||
def connect_entrances_lfod(multiworld, player):
|
||||
multiworld.get_entrance("Live Freemium or Die", player).connect(multiworld.get_region("Freemium Start", player))
|
||||
multiworld.get_entrance("Vines", player).connect(multiworld.get_region("Behind the Vines", player))
|
||||
multiworld.get_entrance("Wall Jump Entrance", player).connect(multiworld.get_region("Wall Jump", player))
|
||||
multiworld.get_entrance("Harmless Plants", player).connect(multiworld.get_region("Fake Ending", player))
|
||||
multiworld.get_entrance("Pickaxe Hard Cave", player).connect(multiworld.get_region("Hard Cave", player))
|
||||
multiworld.get_entrance("Hard Cave Wall Jump", player).connect(multiworld.get_region("Hard Cave Wall Jump", player))
|
||||
multiworld.get_entrance("Name Change Entrance", player).connect(multiworld.get_region("Name Change", player))
|
||||
multiworld.get_entrance("Cut Content Entrance", player).connect(multiworld.get_region("Cut Content", player))
|
||||
multiworld.get_entrance("Behind Rocks", player).connect(multiworld.get_region("Top Right", player))
|
||||
multiworld.get_entrance("Blizzard", player).connect(multiworld.get_region("Season", player))
|
||||
multiworld.get_entrance("Boss Door", player).connect(multiworld.get_region("Final Boss", player))
|
||||
|
||||
world.get_entrance("Behind Ogre", player).connect(world.get_region("The Final Boss Room", player))
|
||||
|
||||
world.get_entrance("Forest Double Jump", player).connect(
|
||||
world.get_region("The Forest whit double Jump", player))
|
||||
def create_coinsanity_locations_dlc_quest(has_coinsanity: bool, coin_bundle_size: int, player: int, region_move_right: Region):
|
||||
create_coinsanity_locations(has_coinsanity, coin_bundle_size, player, region_move_right, 825, "DLC Quest")
|
||||
|
||||
world.get_entrance("Forest True Double Jump", player).connect(
|
||||
world.get_region("The Forest whit double Jump Part 2", player))
|
||||
|
||||
world.get_entrance("True Double Jump", player).connect(world.get_region("True Double Jump Behind Tree", player))
|
||||
def create_coinsanity_locations_lfod(has_coinsanity: bool, coin_bundle_size: int, player: int, region_lfod_start: Region):
|
||||
create_coinsanity_locations(has_coinsanity, coin_bundle_size, player, region_lfod_start, 889, "Live Freemium or Die")
|
||||
|
||||
if (World_Options.campaign == Options.Campaign.option_live_freemium_or_die or World_Options.campaign
|
||||
== Options.Campaign.option_both):
|
||||
|
||||
Regfreemiumstart = Region("Freemium Start", player, world)
|
||||
Locfreemiumstart_name = ["Particles Pack", "Day One Patch Pack", "Checkpoint Pack", "Incredibly Important Pack",
|
||||
"Nice Try", "Story is Important", "I Get That Reference!"]
|
||||
if World_Options.item_shuffle == Options.ItemShuffle.option_shuffled:
|
||||
Locfreemiumstart_name += ["Wooden Sword"]
|
||||
Regfreemiumstart.exits = [Entrance(player, "Vines", Regfreemiumstart)]
|
||||
Regfreemiumstart.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regfreemiumstart)
|
||||
for loc_name in
|
||||
Locfreemiumstart_name]
|
||||
add_coin_freemium(Regfreemiumstart, 50, player)
|
||||
if World_Options.coinsanity == Options.CoinSanity.option_coin:
|
||||
coin_bundle_needed = math.floor(889 / World_Options.coinbundlequantity)
|
||||
for i in range(coin_bundle_needed):
|
||||
item_coin_freemium = f"Live Freemium or Die: {World_Options.coinbundlequantity * (i + 1)} Coin"
|
||||
Regfreemiumstart.locations += [
|
||||
DLCQuestLocation(player, item_coin_freemium, location_table[item_coin_freemium],
|
||||
Regfreemiumstart)]
|
||||
if 889 % World_Options.coinbundlequantity != 0:
|
||||
Regfreemiumstart.locations += [
|
||||
DLCQuestLocation(player, "Live Freemium or Die: 889 Coin",
|
||||
location_table["Live Freemium or Die: 889 Coin"],
|
||||
Regfreemiumstart)]
|
||||
world.regions.append(Regfreemiumstart)
|
||||
def create_coinsanity_locations(has_coinsanity: bool, coin_bundle_size: int, player: int, region: Region, last_coin_number: int, campaign_prefix: str):
|
||||
if not has_coinsanity:
|
||||
return
|
||||
|
||||
Regbehindvine = Region("Behind the Vines", player, world)
|
||||
Locbehindvine_name = ["Wall Jump Pack", "Health Bar Pack", "Parallax Pack"]
|
||||
if World_Options.item_shuffle == Options.ItemShuffle.option_shuffled:
|
||||
Locbehindvine_name += ["Pickaxe"]
|
||||
Regbehindvine.exits = [Entrance(player, "Wall Jump Entrance", Regbehindvine)]
|
||||
Regbehindvine.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regbehindvine) for
|
||||
loc_name in Locbehindvine_name]
|
||||
add_coin_freemium(Regbehindvine, 95, player)
|
||||
world.regions.append(Regbehindvine)
|
||||
|
||||
Regwalljump = Region("Wall Jump", player, world)
|
||||
Locwalljump_name = ["Harmless Plants Pack", "Death of Comedy Pack", "Canadian Dialog Pack", "DLC NPC Pack"]
|
||||
Regwalljump.exits = [Entrance(player, "Harmless Plants", Regwalljump),
|
||||
Entrance(player, "Pickaxe Hard Cave", Regwalljump)]
|
||||
Regwalljump.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regwalljump) for
|
||||
loc_name in Locwalljump_name]
|
||||
add_coin_freemium(Regwalljump, 150, player)
|
||||
world.regions.append(Regwalljump)
|
||||
|
||||
Regfakeending = Region("Fake Ending", player, world)
|
||||
Locfakeending_name = ["Cut Content Pack", "Name Change Pack"]
|
||||
Regfakeending.exits = [Entrance(player, "Name Change Entrance", Regfakeending),
|
||||
Entrance(player, "Cut Content Entrance", Regfakeending)]
|
||||
Regfakeending.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regfakeending) for
|
||||
loc_name in Locfakeending_name]
|
||||
world.regions.append(Regfakeending)
|
||||
|
||||
Reghardcave = Region("Hard Cave", player, world)
|
||||
add_coin_freemium(Reghardcave, 20, player)
|
||||
Reghardcave.exits = [Entrance(player, "Hard Cave Wall Jump", Reghardcave)]
|
||||
world.regions.append(Reghardcave)
|
||||
|
||||
Reghardcavewalljump = Region("Hard Cave Wall Jump", player, world)
|
||||
Lochardcavewalljump_name = ["Increased HP Pack"]
|
||||
Reghardcavewalljump.locations += [
|
||||
DLCQuestLocation(player, loc_name, location_table[loc_name], Reghardcavewalljump) for
|
||||
loc_name in Lochardcavewalljump_name]
|
||||
add_coin_freemium(Reghardcavewalljump, 130, player)
|
||||
world.regions.append(Reghardcavewalljump)
|
||||
|
||||
Regcutcontent = Region("Cut Content", player, world)
|
||||
Loccutcontent_name = []
|
||||
if World_Options.item_shuffle == Options.ItemShuffle.option_shuffled:
|
||||
Loccutcontent_name += ["Humble Indie Bindle"]
|
||||
Regcutcontent.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regcutcontent) for
|
||||
loc_name in Loccutcontent_name]
|
||||
add_coin_freemium(Regcutcontent, 200, player)
|
||||
world.regions.append(Regcutcontent)
|
||||
|
||||
Regnamechange = Region("Name Change", player, world)
|
||||
Locnamechange_name = []
|
||||
if World_Options.item_shuffle == Options.ItemShuffle.option_shuffled:
|
||||
Locnamechange_name += ["Box of Various Supplies"]
|
||||
Regnamechange.exits = [Entrance(player, "Behind Rocks", Regnamechange)]
|
||||
Regnamechange.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regnamechange) for
|
||||
loc_name in Locnamechange_name]
|
||||
world.regions.append(Regnamechange)
|
||||
|
||||
Regtopright = Region("Top Right", player, world)
|
||||
Loctopright_name = ["Season Pass", "High Definition Next Gen Pack"]
|
||||
Regtopright.exits = [Entrance(player, "Blizzard", Regtopright)]
|
||||
Regtopright.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regtopright) for
|
||||
loc_name in Loctopright_name]
|
||||
add_coin_freemium(Regtopright, 90, player)
|
||||
world.regions.append(Regtopright)
|
||||
|
||||
Regseason = Region("Season", player, world)
|
||||
Locseason_name = ["Remove Ads Pack", "Not Exactly Noble"]
|
||||
Regseason.exits = [Entrance(player, "Boss Door", Regseason)]
|
||||
Regseason.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regseason) for
|
||||
loc_name in Locseason_name]
|
||||
add_coin_freemium(Regseason, 154, player)
|
||||
world.regions.append(Regseason)
|
||||
|
||||
Regfinalboss = Region("Final Boss", player, world)
|
||||
Locfinalboss_name = ["Big Sword Pack", "Really Big Sword Pack", "Unfathomable Sword Pack"]
|
||||
Regfinalboss.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regfinalboss) for
|
||||
loc_name in Locfinalboss_name]
|
||||
world.regions.append(Regfinalboss)
|
||||
|
||||
loc_wining = DLCQuestLocation(player, "Winning Freemium", None, world.get_region("Final Boss", player))
|
||||
world.get_region("Final Boss", player).locations.append(loc_wining)
|
||||
loc_wining.place_locked_item(create_event(player, "Victory Freemium"))
|
||||
|
||||
world.get_entrance("Live Freemium or Die", player).connect(world.get_region("Freemium Start", player))
|
||||
|
||||
world.get_entrance("Vines", player).connect(world.get_region("Behind the Vines", player))
|
||||
|
||||
world.get_entrance("Wall Jump Entrance", player).connect(world.get_region("Wall Jump", player))
|
||||
|
||||
world.get_entrance("Harmless Plants", player).connect(world.get_region("Fake Ending", player))
|
||||
|
||||
world.get_entrance("Pickaxe Hard Cave", player).connect(world.get_region("Hard Cave", player))
|
||||
|
||||
world.get_entrance("Hard Cave Wall Jump", player).connect(world.get_region("Hard Cave Wall Jump", player))
|
||||
|
||||
world.get_entrance("Name Change Entrance", player).connect(world.get_region("Name Change", player))
|
||||
|
||||
world.get_entrance("Cut Content Entrance", player).connect(world.get_region("Cut Content", player))
|
||||
|
||||
world.get_entrance("Behind Rocks", player).connect(world.get_region("Top Right", player))
|
||||
|
||||
world.get_entrance("Blizzard", player).connect(world.get_region("Season", player))
|
||||
|
||||
world.get_entrance("Boss Door", player).connect(world.get_region("Final Boss", player))
|
||||
coin_bundle_needed = math.ceil(last_coin_number / coin_bundle_size)
|
||||
for i in range(1, coin_bundle_needed + 1):
|
||||
number_coins = min(last_coin_number, coin_bundle_size * i)
|
||||
item_coin = f"{campaign_prefix}: {number_coins} Coin"
|
||||
region.locations += [DLCQuestLocation(player, item_coin, location_table[item_coin], region)]
|
||||
|
||||
@@ -7,41 +7,25 @@ from . import Options
|
||||
from .Items import DLCQuestItem
|
||||
|
||||
|
||||
def create_event(player, event: str):
|
||||
def create_event(player, event: str) -> DLCQuestItem:
|
||||
return DLCQuestItem(event, ItemClassification.progression, None, player)
|
||||
|
||||
|
||||
def has_enough_coin(player: int, coin: int):
|
||||
return lambda state: state.prog_items[" coins", player] >= coin
|
||||
|
||||
|
||||
def has_enough_coin_freemium(player: int, coin: int):
|
||||
return lambda state: state.prog_items[" coins freemium", player] >= coin
|
||||
|
||||
|
||||
def set_rules(world, player, World_Options: Options.DLCQuestOptions):
|
||||
def has_enough_coin(player: int, coin: int):
|
||||
def has_coin(state, player: int, coins: int):
|
||||
coin_possessed = 0
|
||||
for i in [4, 7, 9, 10, 46, 50, 60, 76, 89, 100, 171, 203]:
|
||||
name_coin = f"{i} coins"
|
||||
if state.has(name_coin, player):
|
||||
coin_possessed += i
|
||||
|
||||
return coin_possessed >= coins
|
||||
|
||||
return lambda state: has_coin(state, player, coin)
|
||||
|
||||
def has_enough_coin_freemium(player: int, coin: int):
|
||||
def has_coin(state, player: int, coins: int):
|
||||
coin_possessed = 0
|
||||
for i in [20, 50, 90, 95, 130, 150, 154, 200]:
|
||||
name_coin = f"{i} coins freemium"
|
||||
if state.has(name_coin, player):
|
||||
coin_possessed += i
|
||||
|
||||
return coin_possessed >= coins
|
||||
|
||||
return lambda state: has_coin(state, player, coin)
|
||||
|
||||
set_basic_rules(World_Options, has_enough_coin, player, world)
|
||||
set_lfod_rules(World_Options, has_enough_coin_freemium, player, world)
|
||||
set_basic_rules(World_Options, player, world)
|
||||
set_lfod_rules(World_Options, player, world)
|
||||
set_completion_condition(World_Options, player, world)
|
||||
|
||||
|
||||
def set_basic_rules(World_Options, has_enough_coin, player, world):
|
||||
def set_basic_rules(World_Options, player, world):
|
||||
if World_Options.campaign == Options.Campaign.option_live_freemium_or_die:
|
||||
return
|
||||
set_basic_entrance_rules(player, world)
|
||||
@@ -49,8 +33,8 @@ def set_basic_rules(World_Options, has_enough_coin, player, world):
|
||||
set_basic_shuffled_items_rules(World_Options, player, world)
|
||||
set_double_jump_glitchless_rules(World_Options, player, world)
|
||||
set_easy_double_jump_glitch_rules(World_Options, player, world)
|
||||
self_basic_coinsanity_funded_purchase_rules(World_Options, has_enough_coin, player, world)
|
||||
set_basic_self_funded_purchase_rules(World_Options, has_enough_coin, player, world)
|
||||
self_basic_coinsanity_funded_purchase_rules(World_Options, player, world)
|
||||
set_basic_self_funded_purchase_rules(World_Options, player, world)
|
||||
self_basic_win_condition(World_Options, player, world)
|
||||
|
||||
|
||||
@@ -131,7 +115,7 @@ def set_easy_double_jump_glitch_rules(World_Options, player, world):
|
||||
lambda state: state.has("Double Jump Pack", player))
|
||||
|
||||
|
||||
def self_basic_coinsanity_funded_purchase_rules(World_Options, has_enough_coin, player, world):
|
||||
def self_basic_coinsanity_funded_purchase_rules(World_Options, player, world):
|
||||
if World_Options.coinsanity != Options.CoinSanity.option_coin:
|
||||
return
|
||||
number_of_bundle = math.floor(825 / World_Options.coinbundlequantity)
|
||||
@@ -194,7 +178,7 @@ def self_basic_coinsanity_funded_purchase_rules(World_Options, has_enough_coin,
|
||||
math.ceil(5 / World_Options.coinbundlequantity)))
|
||||
|
||||
|
||||
def set_basic_self_funded_purchase_rules(World_Options, has_enough_coin, player, world):
|
||||
def set_basic_self_funded_purchase_rules(World_Options, player, world):
|
||||
if World_Options.coinsanity != Options.CoinSanity.option_none:
|
||||
return
|
||||
set_rule(world.get_location("Movement Pack", player),
|
||||
@@ -241,14 +225,14 @@ def self_basic_win_condition(World_Options, player, world):
|
||||
player))
|
||||
|
||||
|
||||
def set_lfod_rules(World_Options, has_enough_coin_freemium, player, world):
|
||||
def set_lfod_rules(World_Options, player, world):
|
||||
if World_Options.campaign == Options.Campaign.option_basic:
|
||||
return
|
||||
set_lfod_entrance_rules(player, world)
|
||||
set_boss_door_requirements_rules(player, world)
|
||||
set_lfod_self_obtained_items_rules(World_Options, player, world)
|
||||
set_lfod_shuffled_items_rules(World_Options, player, world)
|
||||
self_lfod_coinsanity_funded_purchase_rules(World_Options, has_enough_coin_freemium, player, world)
|
||||
self_lfod_coinsanity_funded_purchase_rules(World_Options, player, world)
|
||||
set_lfod_self_funded_purchase_rules(World_Options, has_enough_coin_freemium, player, world)
|
||||
|
||||
|
||||
@@ -327,7 +311,7 @@ def set_lfod_shuffled_items_rules(World_Options, player, world):
|
||||
lambda state: state.can_reach("Cut Content", 'region', player))
|
||||
|
||||
|
||||
def self_lfod_coinsanity_funded_purchase_rules(World_Options, has_enough_coin_freemium, player, world):
|
||||
def self_lfod_coinsanity_funded_purchase_rules(World_Options, player, world):
|
||||
if World_Options.coinsanity != Options.CoinSanity.option_coin:
|
||||
return
|
||||
number_of_bundle = math.floor(889 / World_Options.coinbundlequantity)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from typing import Union
|
||||
|
||||
from BaseClasses import Tutorial
|
||||
from BaseClasses import Tutorial, CollectionState
|
||||
from worlds.AutoWorld import WebWorld, World
|
||||
from . import Options
|
||||
from .Items import DLCQuestItem, ItemData, create_items, item_table
|
||||
@@ -71,7 +71,6 @@ class DLCqworld(World):
|
||||
if self.options.coinsanity == Options.CoinSanity.option_coin and self.options.coinbundlequantity >= 5:
|
||||
self.multiworld.push_precollected(self.create_item("Movement Pack"))
|
||||
|
||||
|
||||
def create_item(self, item: Union[str, ItemData]) -> DLCQuestItem:
|
||||
if isinstance(item, str):
|
||||
item = item_table[item]
|
||||
@@ -87,3 +86,19 @@ class DLCqworld(World):
|
||||
"seed": self.random.randrange(99999999)
|
||||
})
|
||||
return options_dict
|
||||
|
||||
def collect(self, state: CollectionState, item: DLCQuestItem) -> bool:
|
||||
change = super().collect(state, item)
|
||||
if change:
|
||||
suffix = item.coin_suffix
|
||||
if suffix:
|
||||
state.prog_items[suffix, self.player] += item.coins
|
||||
return change
|
||||
|
||||
def remove(self, state: CollectionState, item: DLCQuestItem) -> bool:
|
||||
change = super().remove(state, item)
|
||||
if change:
|
||||
suffix = item.coin_suffix
|
||||
if suffix:
|
||||
state.prog_items[suffix, self.player] -= item.coins
|
||||
return change
|
||||
|
||||
130
worlds/dlcquest/test/TestItemShuffle.py
Normal file
@@ -0,0 +1,130 @@
|
||||
from . import DLCQuestTestBase
|
||||
from .. import Options
|
||||
|
||||
sword = "Sword"
|
||||
gun = "Gun"
|
||||
wooden_sword = "Wooden Sword"
|
||||
pickaxe = "Pickaxe"
|
||||
humble_bindle = "Humble Indie Bindle"
|
||||
box_supplies = "Box of Various Supplies"
|
||||
items = [sword, gun, wooden_sword, pickaxe, humble_bindle, box_supplies]
|
||||
|
||||
important_pack = "Incredibly Important Pack"
|
||||
|
||||
|
||||
class TestItemShuffle(DLCQuestTestBase):
|
||||
options = {Options.ItemShuffle.internal_name: Options.ItemShuffle.option_shuffled,
|
||||
Options.Campaign.internal_name: Options.Campaign.option_both}
|
||||
|
||||
def test_items_in_pool(self):
|
||||
item_names = {item.name for item in self.multiworld.get_items()}
|
||||
for item in items:
|
||||
with self.subTest(f"{item}"):
|
||||
self.assertIn(item, item_names)
|
||||
|
||||
def test_item_locations_in_pool(self):
|
||||
location_names = {location.name for location in self.multiworld.get_locations()}
|
||||
for item_location in items:
|
||||
with self.subTest(f"{item_location}"):
|
||||
self.assertIn(item_location, location_names)
|
||||
|
||||
def test_sword_location_has_correct_rules(self):
|
||||
self.assertFalse(self.can_reach_location(sword))
|
||||
movement_pack = self.multiworld.create_item("Movement Pack", self.player)
|
||||
self.collect(movement_pack)
|
||||
self.assertFalse(self.can_reach_location(sword))
|
||||
time_pack = self.multiworld.create_item("Time is Money Pack", self.player)
|
||||
self.collect(time_pack)
|
||||
self.assertTrue(self.can_reach_location(sword))
|
||||
|
||||
def test_gun_location_has_correct_rules(self):
|
||||
self.assertFalse(self.can_reach_location(gun))
|
||||
movement_pack = self.multiworld.create_item("Movement Pack", self.player)
|
||||
self.collect(movement_pack)
|
||||
self.assertFalse(self.can_reach_location(gun))
|
||||
sword_item = self.multiworld.create_item(sword, self.player)
|
||||
self.collect(sword_item)
|
||||
self.assertFalse(self.can_reach_location(gun))
|
||||
gun_pack = self.multiworld.create_item("Gun Pack", self.player)
|
||||
self.collect(gun_pack)
|
||||
self.assertTrue(self.can_reach_location(gun))
|
||||
|
||||
def test_wooden_sword_location_has_correct_rules(self):
|
||||
self.assertFalse(self.can_reach_location(wooden_sword))
|
||||
important_pack_item = self.multiworld.create_item(important_pack, self.player)
|
||||
self.collect(important_pack_item)
|
||||
self.assertTrue(self.can_reach_location(wooden_sword))
|
||||
|
||||
def test_bindle_location_has_correct_rules(self):
|
||||
self.assertFalse(self.can_reach_location(humble_bindle))
|
||||
wooden_sword_item = self.multiworld.create_item(wooden_sword, self.player)
|
||||
self.collect(wooden_sword_item)
|
||||
self.assertFalse(self.can_reach_location(humble_bindle))
|
||||
plants_pack = self.multiworld.create_item("Harmless Plants Pack", self.player)
|
||||
self.collect(plants_pack)
|
||||
self.assertFalse(self.can_reach_location(humble_bindle))
|
||||
wall_jump_pack = self.multiworld.create_item("Wall Jump Pack", self.player)
|
||||
self.collect(wall_jump_pack)
|
||||
self.assertFalse(self.can_reach_location(humble_bindle))
|
||||
name_change_pack = self.multiworld.create_item("Name Change Pack", self.player)
|
||||
self.collect(name_change_pack)
|
||||
self.assertFalse(self.can_reach_location(humble_bindle))
|
||||
cut_content_pack = self.multiworld.create_item("Cut Content Pack", self.player)
|
||||
self.collect(cut_content_pack)
|
||||
self.assertFalse(self.can_reach_location(humble_bindle))
|
||||
box_supplies_item = self.multiworld.create_item(box_supplies, self.player)
|
||||
self.collect(box_supplies_item)
|
||||
self.assertTrue(self.can_reach_location(humble_bindle))
|
||||
|
||||
def test_box_supplies_location_has_correct_rules(self):
|
||||
self.assertFalse(self.can_reach_location(box_supplies))
|
||||
wooden_sword_item = self.multiworld.create_item(wooden_sword, self.player)
|
||||
self.collect(wooden_sword_item)
|
||||
self.assertFalse(self.can_reach_location(box_supplies))
|
||||
plants_pack = self.multiworld.create_item("Harmless Plants Pack", self.player)
|
||||
self.collect(plants_pack)
|
||||
self.assertFalse(self.can_reach_location(box_supplies))
|
||||
wall_jump_pack = self.multiworld.create_item("Wall Jump Pack", self.player)
|
||||
self.collect(wall_jump_pack)
|
||||
self.assertFalse(self.can_reach_location(box_supplies))
|
||||
name_change_pack = self.multiworld.create_item("Name Change Pack", self.player)
|
||||
self.collect(name_change_pack)
|
||||
self.assertFalse(self.can_reach_location(box_supplies))
|
||||
cut_content_pack = self.multiworld.create_item("Cut Content Pack", self.player)
|
||||
self.collect(cut_content_pack)
|
||||
self.assertTrue(self.can_reach_location(box_supplies))
|
||||
|
||||
def test_pickaxe_location_has_correct_rules(self):
|
||||
self.assertFalse(self.can_reach_location(pickaxe))
|
||||
wooden_sword_item = self.multiworld.create_item(wooden_sword, self.player)
|
||||
self.collect(wooden_sword_item)
|
||||
self.assertFalse(self.can_reach_location(pickaxe))
|
||||
plants_pack = self.multiworld.create_item("Harmless Plants Pack", self.player)
|
||||
self.collect(plants_pack)
|
||||
self.assertFalse(self.can_reach_location(pickaxe))
|
||||
wall_jump_pack = self.multiworld.create_item("Wall Jump Pack", self.player)
|
||||
self.collect(wall_jump_pack)
|
||||
self.assertFalse(self.can_reach_location(pickaxe))
|
||||
name_change_pack = self.multiworld.create_item("Name Change Pack", self.player)
|
||||
self.collect(name_change_pack)
|
||||
self.assertFalse(self.can_reach_location(pickaxe))
|
||||
bindle_item = self.multiworld.create_item("Humble Indie Bindle", self.player)
|
||||
self.collect(bindle_item)
|
||||
self.assertTrue(self.can_reach_location(pickaxe))
|
||||
|
||||
|
||||
class TestNoItemShuffle(DLCQuestTestBase):
|
||||
options = {Options.ItemShuffle.internal_name: Options.ItemShuffle.option_disabled,
|
||||
Options.Campaign.internal_name: Options.Campaign.option_both}
|
||||
|
||||
def test_items_not_in_pool(self):
|
||||
item_names = {item.name for item in self.multiworld.get_items()}
|
||||
for item in items:
|
||||
with self.subTest(f"{item}"):
|
||||
self.assertNotIn(item, item_names)
|
||||
|
||||
def test_item_locations_not_in_pool(self):
|
||||
location_names = {location.name for location in self.multiworld.get_locations()}
|
||||
for item_location in items:
|
||||
with self.subTest(f"{item_location}"):
|
||||
self.assertNotIn(item_location, location_names)
|
||||
87
worlds/dlcquest/test/TestOptionsLong.py
Normal file
@@ -0,0 +1,87 @@
|
||||
from typing import Dict
|
||||
|
||||
from BaseClasses import MultiWorld
|
||||
from Options import SpecialRange
|
||||
from .option_names import options_to_include
|
||||
from .checks.world_checks import assert_can_win, assert_same_number_items_locations
|
||||
from . import DLCQuestTestBase, setup_dlc_quest_solo_multiworld
|
||||
from ... import AutoWorldRegister
|
||||
|
||||
|
||||
def basic_checks(tester: DLCQuestTestBase, multiworld: MultiWorld):
|
||||
assert_can_win(tester, multiworld)
|
||||
assert_same_number_items_locations(tester, multiworld)
|
||||
|
||||
|
||||
def get_option_choices(option) -> Dict[str, int]:
|
||||
if issubclass(option, SpecialRange):
|
||||
return option.special_range_names
|
||||
elif option.options:
|
||||
return option.options
|
||||
return {}
|
||||
|
||||
|
||||
class TestGenerateDynamicOptions(DLCQuestTestBase):
|
||||
def test_given_option_pair_when_generate_then_basic_checks(self):
|
||||
num_options = len(options_to_include)
|
||||
for option1_index in range(0, num_options):
|
||||
for option2_index in range(option1_index + 1, num_options):
|
||||
option1 = options_to_include[option1_index]
|
||||
option2 = options_to_include[option2_index]
|
||||
option1_choices = get_option_choices(option1)
|
||||
option2_choices = get_option_choices(option2)
|
||||
for key1 in option1_choices:
|
||||
for key2 in option2_choices:
|
||||
with self.subTest(f"{option1.internal_name}: {key1}, {option2.internal_name}: {key2}"):
|
||||
choices = {option1.internal_name: option1_choices[key1],
|
||||
option2.internal_name: option2_choices[key2]}
|
||||
multiworld = setup_dlc_quest_solo_multiworld(choices)
|
||||
basic_checks(self, multiworld)
|
||||
|
||||
def test_given_option_truple_when_generate_then_basic_checks(self):
|
||||
num_options = len(options_to_include)
|
||||
for option1_index in range(0, num_options):
|
||||
for option2_index in range(option1_index + 1, num_options):
|
||||
for option3_index in range(option2_index + 1, num_options):
|
||||
option1 = options_to_include[option1_index]
|
||||
option2 = options_to_include[option2_index]
|
||||
option3 = options_to_include[option3_index]
|
||||
option1_choices = get_option_choices(option1)
|
||||
option2_choices = get_option_choices(option2)
|
||||
option3_choices = get_option_choices(option3)
|
||||
for key1 in option1_choices:
|
||||
for key2 in option2_choices:
|
||||
for key3 in option3_choices:
|
||||
with self.subTest(f"{option1.internal_name}: {key1}, {option2.internal_name}: {key2}, {option3.internal_name}: {key3}"):
|
||||
choices = {option1.internal_name: option1_choices[key1],
|
||||
option2.internal_name: option2_choices[key2],
|
||||
option3.internal_name: option3_choices[key3]}
|
||||
multiworld = setup_dlc_quest_solo_multiworld(choices)
|
||||
basic_checks(self, multiworld)
|
||||
|
||||
def test_given_option_quartet_when_generate_then_basic_checks(self):
|
||||
num_options = len(options_to_include)
|
||||
for option1_index in range(0, num_options):
|
||||
for option2_index in range(option1_index + 1, num_options):
|
||||
for option3_index in range(option2_index + 1, num_options):
|
||||
for option4_index in range(option3_index + 1, num_options):
|
||||
option1 = options_to_include[option1_index]
|
||||
option2 = options_to_include[option2_index]
|
||||
option3 = options_to_include[option3_index]
|
||||
option4 = options_to_include[option4_index]
|
||||
option1_choices = get_option_choices(option1)
|
||||
option2_choices = get_option_choices(option2)
|
||||
option3_choices = get_option_choices(option3)
|
||||
option4_choices = get_option_choices(option4)
|
||||
for key1 in option1_choices:
|
||||
for key2 in option2_choices:
|
||||
for key3 in option3_choices:
|
||||
for key4 in option4_choices:
|
||||
with self.subTest(
|
||||
f"{option1.internal_name}: {key1}, {option2.internal_name}: {key2}, {option3.internal_name}: {key3}, {option4.internal_name}: {key4}"):
|
||||
choices = {option1.internal_name: option1_choices[key1],
|
||||
option2.internal_name: option2_choices[key2],
|
||||
option3.internal_name: option3_choices[key3],
|
||||
option4.internal_name: option4_choices[key4]}
|
||||
multiworld = setup_dlc_quest_solo_multiworld(choices)
|
||||
basic_checks(self, multiworld)
|
||||
53
worlds/dlcquest/test/__init__.py
Normal file
@@ -0,0 +1,53 @@
|
||||
from typing import ClassVar
|
||||
|
||||
from typing import Dict, FrozenSet, Tuple, Any
|
||||
from argparse import Namespace
|
||||
|
||||
from BaseClasses import MultiWorld
|
||||
from test.TestBase import WorldTestBase
|
||||
from .. import DLCqworld
|
||||
from test.general import gen_steps, setup_solo_multiworld as setup_base_solo_multiworld
|
||||
from worlds.AutoWorld import call_all
|
||||
|
||||
|
||||
class DLCQuestTestBase(WorldTestBase):
|
||||
game = "DLCQuest"
|
||||
world: DLCqworld
|
||||
player: ClassVar[int] = 1
|
||||
|
||||
def world_setup(self, *args, **kwargs):
|
||||
super().world_setup(*args, **kwargs)
|
||||
if self.constructed:
|
||||
self.world = self.multiworld.worlds[self.player] # noqa
|
||||
|
||||
@property
|
||||
def run_default_tests(self) -> bool:
|
||||
# world_setup is overridden, so it'd always run default tests when importing DLCQuestTestBase
|
||||
is_not_dlc_test = type(self) is not DLCQuestTestBase
|
||||
should_run_default_tests = is_not_dlc_test and super().run_default_tests
|
||||
return should_run_default_tests
|
||||
|
||||
|
||||
def setup_dlc_quest_solo_multiworld(test_options=None, seed=None, _cache: Dict[FrozenSet[Tuple[str, Any]], MultiWorld] = {}) -> MultiWorld: #noqa
|
||||
if test_options is None:
|
||||
test_options = {}
|
||||
|
||||
# Yes I reuse the worlds generated between tests, its speeds the execution by a couple seconds
|
||||
frozen_options = frozenset(test_options.items()).union({seed})
|
||||
if frozen_options in _cache:
|
||||
return _cache[frozen_options]
|
||||
|
||||
multiworld = setup_base_solo_multiworld(DLCqworld, ())
|
||||
multiworld.set_seed(seed)
|
||||
# print(f"Seed: {multiworld.seed}") # Uncomment to print the seed for every test
|
||||
args = Namespace()
|
||||
for name, option in DLCqworld.options_dataclass.type_hints.items():
|
||||
value = option(test_options[name]) if name in test_options else option.from_any(option.default)
|
||||
setattr(args, name, {1: value})
|
||||
multiworld.set_options(args)
|
||||
for step in gen_steps:
|
||||
call_all(multiworld, step)
|
||||
|
||||
_cache[frozen_options] = multiworld
|
||||
|
||||
return multiworld
|
||||
0
worlds/dlcquest/test/checks/__init__.py
Normal file
42
worlds/dlcquest/test/checks/world_checks.py
Normal file
@@ -0,0 +1,42 @@
|
||||
from typing import List
|
||||
|
||||
from BaseClasses import MultiWorld, ItemClassification
|
||||
from .. import DLCQuestTestBase
|
||||
from ... import Options
|
||||
|
||||
|
||||
def get_all_item_names(multiworld: MultiWorld) -> List[str]:
|
||||
return [item.name for item in multiworld.itempool]
|
||||
|
||||
|
||||
def get_all_location_names(multiworld: MultiWorld) -> List[str]:
|
||||
return [location.name for location in multiworld.get_locations() if not location.event]
|
||||
|
||||
|
||||
def assert_victory_exists(tester: DLCQuestTestBase, multiworld: MultiWorld):
|
||||
campaign = multiworld.campaign[1]
|
||||
all_items = [item.name for item in multiworld.get_items()]
|
||||
if campaign == Options.Campaign.option_basic or campaign == Options.Campaign.option_both:
|
||||
tester.assertIn("Victory Basic", all_items)
|
||||
if campaign == Options.Campaign.option_live_freemium_or_die or campaign == Options.Campaign.option_both:
|
||||
tester.assertIn("Victory Freemium", all_items)
|
||||
|
||||
|
||||
def collect_all_then_assert_can_win(tester: DLCQuestTestBase, multiworld: MultiWorld):
|
||||
for item in multiworld.get_items():
|
||||
multiworld.state.collect(item)
|
||||
campaign = multiworld.campaign[1]
|
||||
if campaign == Options.Campaign.option_basic or campaign == Options.Campaign.option_both:
|
||||
tester.assertTrue(multiworld.find_item("Victory Basic", 1).can_reach(multiworld.state))
|
||||
if campaign == Options.Campaign.option_live_freemium_or_die or campaign == Options.Campaign.option_both:
|
||||
tester.assertTrue(multiworld.find_item("Victory Freemium", 1).can_reach(multiworld.state))
|
||||
|
||||
|
||||
def assert_can_win(tester: DLCQuestTestBase, multiworld: MultiWorld):
|
||||
assert_victory_exists(tester, multiworld)
|
||||
collect_all_then_assert_can_win(tester, multiworld)
|
||||
|
||||
|
||||
def assert_same_number_items_locations(tester: DLCQuestTestBase, multiworld: MultiWorld):
|
||||
non_event_locations = [location for location in multiworld.get_locations() if not location.event]
|
||||
tester.assertEqual(len(multiworld.itempool), len(non_event_locations))
|
||||
5
worlds/dlcquest/test/option_names.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from .. import DLCqworld
|
||||
|
||||
options_to_exclude = ["progression_balancing", "accessibility", "start_inventory", "start_hints", "death_link"]
|
||||
options_to_include = [option for option_name, option in DLCqworld.options_dataclass.type_hints.items()
|
||||
if option_name not in options_to_exclude]
|
||||
@@ -31,7 +31,7 @@ them. Factorio player settings page: [Factorio Settings Page](/games/Factorio/pl
|
||||
### Verifying your config file
|
||||
|
||||
If you would like to validate your config file to make sure it works, you may do so on the YAML Validator page. YAML
|
||||
Validator page: [Yaml Validation Page](/mysterycheck)
|
||||
Validator page: [Yaml Validation Page](/check)
|
||||
|
||||
## Connecting to Someone Else's Factorio Game
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ class FF1Settings(settings.Group):
|
||||
|
||||
|
||||
class FF1Web(WebWorld):
|
||||
settings_page = "https://finalfantasyrandomizer.com/"
|
||||
options_page = "https://finalfantasyrandomizer.com/"
|
||||
tutorials = [Tutorial(
|
||||
"Multiworld Setup Guide",
|
||||
"A guide to playing Final Fantasy multiworld. This guide only covers playing multiworld.",
|
||||
@@ -91,7 +91,7 @@ class FF1World(World):
|
||||
def set_rules(self):
|
||||
self.multiworld.completion_condition[self.player] = lambda state: state.has(CHAOS_TERMINATED_EVENT, self.player)
|
||||
|
||||
def generate_basic(self):
|
||||
def create_items(self):
|
||||
items = get_options(self.multiworld, 'items', self.player)
|
||||
if FF1_BRIDGE in items.keys():
|
||||
self._place_locked_item_in_sphere0(FF1_BRIDGE)
|
||||
|
||||
@@ -108,7 +108,9 @@ guide: [Archipelago Plando Guide](/tutorial/Archipelago/plando/en)
|
||||
* `minimal` will only guarantee that the seed is beatable. You will be guaranteed able to finish the seed logically
|
||||
but may not be able to access all locations or acquire all items. A good example of this is having a big key in
|
||||
the big chest in a dungeon in ALTTP making it impossible to get and finish the dungeon.
|
||||
* `progression_balancing` is a system the Archipelago generator uses to try and reduce "BK mode" as much as possible.
|
||||
* `progression_balancing` is a system the Archipelago generator uses to try and reduce
|
||||
["BK mode"](/glossary/en/#burger-king-/-bk-mode)
|
||||
as much as possible.
|
||||
This primarily involves moving necessary progression items into earlier logic spheres to make the games more
|
||||
accessible so that players almost always have something to do. This can be in a range from 0 to 99, and is 50 by
|
||||
default. This number represents a percentage of the furthest progressible player.
|
||||
@@ -130,7 +132,7 @@ guide: [Archipelago Plando Guide](/tutorial/Archipelago/plando/en)
|
||||
there without using any hint points.
|
||||
* `exclude_locations` lets you define any locations that you don't want to do and during generation will force a "junk"
|
||||
item which isn't necessary for progression to go in these locations.
|
||||
* `priority_locations` is the inverse of `exlcude_locations`, forcing a progression item in the defined locations.
|
||||
* `priority_locations` is the inverse of `exclude_locations`, forcing a progression item in the defined locations.
|
||||
* `item_links` allows players to link their items into a group with the same item link name and game. The items declared
|
||||
in `item_pool` get combined and when an item is found for the group, all players in the group receive it. Item links
|
||||
can also have local and non local items, forcing the items to either be placed within the worlds of the group or in
|
||||
|
||||
@@ -40,7 +40,7 @@ game you will be playing as well as the settings you would like for that game.
|
||||
|
||||
YAML is a format very similar to JSON however it is made to be more human-readable. If you are ever unsure of the
|
||||
validity of your YAML file you may check the file by uploading it to the check page on the Archipelago website:
|
||||
[YAML Validation Page](/mysterycheck)
|
||||
[YAML Validation Page](/check)
|
||||
|
||||
### Creating a YAML
|
||||
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
# Hollow Knight for Archipelago Setup Guide
|
||||
|
||||
## Required Software
|
||||
* Download and unzip the Scarab+ Mod Manager from the [Scarab+ website](https://themulhima.github.io/Scarab/).
|
||||
* Download and unzip the Lumafly Mod Manager from the [Lumafly website](https://themulhima.github.io/Lumafly/).
|
||||
* A legal copy of Hollow Knight.
|
||||
|
||||
## Installing the Archipelago Mod using Scarab+
|
||||
1. Launch Scarab+ and ensure it locates your Hollow Knight installation directory.
|
||||
## Installing the Archipelago Mod using Lumafly
|
||||
1. Launch Lumafly and ensure it locates your Hollow Knight installation directory.
|
||||
2. Click the "Install" button near the "Archipelago" mod entry.
|
||||
* If desired, also install "Archipelago Map Mod" to use as an in-game tracker.
|
||||
3. Launch the game, you're all set!
|
||||
|
||||
### What to do if Scarab+ fails to find your XBox Game Pass installation directory
|
||||
### What to do if Lumafly fails to find your XBox Game Pass installation directory
|
||||
1. Enter the XBox app and move your mouse over "Hollow Knight" on the left sidebar.
|
||||
2. Click the three points then click "Manage".
|
||||
3. Go to the "Files" tab and select "Browse...".
|
||||
4. Click "Hollow Knight", then "Content", then click the path bar and copy it.
|
||||
5. Run Scarab+ as an administrator and, when it asks you for the path, paste what you copied in step 4.
|
||||
5. Run Lumafly as an administrator and, when it asks you for the path, paste what you copied in step 4.
|
||||
|
||||
#### Alternative Method:
|
||||
1. Click on your profile then "Settings".
|
||||
2. Go to the "General" tab and select "CHANGE FOLDER".
|
||||
3. Look for a folder where you want to install the game (preferably inside a folder on your desktop) and copy the path.
|
||||
4. Run Scarab+ as an administrator and, when it asks you for the path, paste what you copied in step 3.
|
||||
4. Run Lumafly as an administrator and, when it asks you for the path, paste what you copied in step 3.
|
||||
|
||||
Note: The path folder needs to have the "Hollow Knight_Data" folder inside.
|
||||
|
||||
|
||||
@@ -1,91 +1,128 @@
|
||||
from worlds.generic.Rules import add_rule
|
||||
from ..AutoWorld import LogicMixin
|
||||
from BaseClasses import CollectionState
|
||||
|
||||
|
||||
class Hylics2Logic(LogicMixin):
|
||||
def air_dash(state: CollectionState, player: int) -> bool:
|
||||
return state.has("PNEUMATOPHORE", player)
|
||||
|
||||
def _hylics2_can_air_dash(self, player):
|
||||
return self.has("PNEUMATOPHORE", player)
|
||||
|
||||
def _hylics2_has_airship(self, player):
|
||||
return self.has("DOCK KEY", player)
|
||||
def airship(state: CollectionState, player: int) -> bool:
|
||||
return state.has("DOCK KEY", player)
|
||||
|
||||
def _hylics2_has_jail_key(self, player):
|
||||
return self.has("JAIL KEY", player)
|
||||
|
||||
def _hylics2_has_paddle(self, player):
|
||||
return self.has("PADDLE", player)
|
||||
def jail_key(state: CollectionState, player: int) -> bool:
|
||||
return state.has("JAIL KEY", player)
|
||||
|
||||
def _hylics2_has_worm_room_key(self, player):
|
||||
return self.has("WORM ROOM KEY", player)
|
||||
|
||||
def _hylics2_has_bridge_key(self, player):
|
||||
return self.has("BRIDGE KEY", player)
|
||||
def paddle(state: CollectionState, player: int) -> bool:
|
||||
return state.has("PADDLE", player)
|
||||
|
||||
def _hylics2_has_upper_chamber_key(self, player):
|
||||
return self.has("UPPER CHAMBER KEY", player)
|
||||
|
||||
def _hylics2_has_vessel_room_key(self, player):
|
||||
return self.has("VESSEL ROOM KEY", player)
|
||||
def worm_room_key(state: CollectionState, player: int) -> bool:
|
||||
return state.has("WORM ROOM KEY", player)
|
||||
|
||||
def _hylics2_has_house_key(self, player):
|
||||
return self.has("HOUSE KEY", player)
|
||||
|
||||
def _hylics2_has_cave_key(self, player):
|
||||
return self.has("CAVE KEY", player)
|
||||
def bridge_key(state: CollectionState, player: int) -> bool:
|
||||
return state.has("BRIDGE KEY", player)
|
||||
|
||||
def _hylics2_has_skull_bomb(self, player):
|
||||
return self.has("SKULL BOMB", player)
|
||||
|
||||
def _hylics2_has_tower_key(self, player):
|
||||
return self.has("TOWER KEY", player)
|
||||
def upper_chamber_key(state: CollectionState, player: int) -> bool:
|
||||
return state.has("UPPER CHAMBER KEY", player)
|
||||
|
||||
def _hylics2_has_deep_key(self, player):
|
||||
return self.has("DEEP KEY", player)
|
||||
|
||||
def _hylics2_has_upper_house_key(self, player):
|
||||
return self.has("UPPER HOUSE KEY", player)
|
||||
def vessel_room_key(state: CollectionState, player: int) -> bool:
|
||||
return state.has("VESSEL ROOM KEY", player)
|
||||
|
||||
def _hylics2_has_clicker(self, player):
|
||||
return self.has("CLICKER", player)
|
||||
|
||||
def _hylics2_has_tokens(self, player):
|
||||
return self.has("SAGE TOKEN", player, 3)
|
||||
def house_key(state: CollectionState, player: int) -> bool:
|
||||
return state.has("HOUSE KEY", player)
|
||||
|
||||
def _hylics2_has_charge_up(self, player):
|
||||
return self.has("CHARGE UP", player)
|
||||
|
||||
def _hylics2_has_cup(self, player):
|
||||
return self.has("PAPER CUP", player, 1)
|
||||
def cave_key(state: CollectionState, player: int) -> bool:
|
||||
return state.has("CAVE KEY", player)
|
||||
|
||||
def _hylics2_has_1_member(self, player):
|
||||
return self.has("Pongorma", player) or self.has("Dedusmuln", player) or self.has("Somsnosa", player)
|
||||
|
||||
def _hylics2_has_2_members(self, player):
|
||||
return (self.has("Pongorma", player) and self.has("Dedusmuln", player)) or\
|
||||
(self.has("Pongorma", player) and self.has("Somsnosa", player)) or\
|
||||
(self.has("Dedusmuln", player) and self.has("Somsnosa", player))
|
||||
def skull_bomb(state: CollectionState, player: int) -> bool:
|
||||
return state.has("SKULL BOMB", player)
|
||||
|
||||
def _hylics2_has_3_members(self, player):
|
||||
return self.has("Pongorma", player) and self.has("Dedusmuln", player) and self.has("Somsnosa", player)
|
||||
|
||||
def _hylics2_enter_arcade2(self, player):
|
||||
return self._hylics2_can_air_dash(player) and self._hylics2_has_airship(player)
|
||||
def tower_key(state: CollectionState, player: int) -> bool:
|
||||
return state.has("TOWER KEY", player)
|
||||
|
||||
def _hylics2_enter_wormpod(self, player):
|
||||
return self._hylics2_has_airship(player) and self._hylics2_has_worm_room_key(player) and\
|
||||
self._hylics2_has_paddle(player)
|
||||
|
||||
def _hylics2_enter_sageship(self, player):
|
||||
return self._hylics2_has_skull_bomb(player) and self._hylics2_has_airship(player) and\
|
||||
self._hylics2_has_paddle(player)
|
||||
def deep_key(state: CollectionState, player: int) -> bool:
|
||||
return state.has("DEEP KEY", player)
|
||||
|
||||
def _hylics2_enter_foglast(self, player):
|
||||
return self._hylics2_enter_wormpod(player)
|
||||
|
||||
def _hylics2_enter_hylemxylem(self, player):
|
||||
return self._hylics2_can_air_dash(player) and self._hylics2_enter_foglast(player) and\
|
||||
self._hylics2_has_bridge_key(player)
|
||||
def upper_house_key(state: CollectionState, player: int) -> bool:
|
||||
return state.has("UPPER HOUSE KEY", player)
|
||||
|
||||
|
||||
def clicker(state: CollectionState, player: int) -> bool:
|
||||
return state.has("CLICKER", player)
|
||||
|
||||
|
||||
def all_tokens(state: CollectionState, player: int) -> bool:
|
||||
return state.has("SAGE TOKEN", player, 3)
|
||||
|
||||
|
||||
def charge_up(state: CollectionState, player: int) -> bool:
|
||||
return state.has("CHARGE UP", player)
|
||||
|
||||
|
||||
def paper_cup(state: CollectionState, player: int) -> bool:
|
||||
return state.has("PAPER CUP", player)
|
||||
|
||||
|
||||
def party_1(state: CollectionState, player: int) -> bool:
|
||||
return state.has_any({"Pongorma", "Dedusmuln", "Somsnosa"}, player)
|
||||
|
||||
|
||||
def party_2(state: CollectionState, player: int) -> bool:
|
||||
return (
|
||||
state.has_all({"Pongorma", "Dedusmuln"}, player)
|
||||
or state.has_all({"Pongorma", "Somsnosa"}, player)
|
||||
or state.has_all({"Dedusmuln", "Somsnosa"}, player)
|
||||
)
|
||||
|
||||
|
||||
def party_3(state: CollectionState, player: int) -> bool:
|
||||
return state.has_all({"Pongorma", "Dedusmuln", "Somsnosa"}, player)
|
||||
|
||||
|
||||
def enter_arcade2(state: CollectionState, player: int) -> bool:
|
||||
return (
|
||||
air_dash(state, player)
|
||||
and airship(state, player)
|
||||
)
|
||||
|
||||
|
||||
def enter_wormpod(state: CollectionState, player: int) -> bool:
|
||||
return (
|
||||
airship(state, player)
|
||||
and worm_room_key(state, player)
|
||||
and paddle(state, player)
|
||||
)
|
||||
|
||||
|
||||
def enter_sageship(state: CollectionState, player: int) -> bool:
|
||||
return (
|
||||
skull_bomb(state, player)
|
||||
and airship(state, player)
|
||||
and paddle(state, player)
|
||||
)
|
||||
|
||||
|
||||
def enter_foglast(state: CollectionState, player: int) -> bool:
|
||||
return enter_wormpod(state, player)
|
||||
|
||||
|
||||
def enter_hylemxylem(state: CollectionState, player: int) -> bool:
|
||||
return (
|
||||
air_dash(state, player)
|
||||
and enter_foglast(state, player)
|
||||
and bridge_key(state, player)
|
||||
)
|
||||
|
||||
|
||||
def set_rules(hylics2world):
|
||||
@@ -94,342 +131,439 @@ def set_rules(hylics2world):
|
||||
|
||||
# Afterlife
|
||||
add_rule(world.get_location("Afterlife: TV", player),
|
||||
lambda state: state._hylics2_has_cave_key(player))
|
||||
lambda state: cave_key(state, player))
|
||||
|
||||
# New Muldul
|
||||
add_rule(world.get_location("New Muldul: Underground Chest", player),
|
||||
lambda state: state._hylics2_can_air_dash(player))
|
||||
lambda state: air_dash(state, player))
|
||||
add_rule(world.get_location("New Muldul: TV", player),
|
||||
lambda state: state._hylics2_has_house_key(player))
|
||||
lambda state: house_key(state, player))
|
||||
add_rule(world.get_location("New Muldul: Upper House Chest 1", player),
|
||||
lambda state: state._hylics2_has_upper_house_key(player))
|
||||
lambda state: upper_house_key(state, player))
|
||||
add_rule(world.get_location("New Muldul: Upper House Chest 2", player),
|
||||
lambda state: state._hylics2_has_upper_house_key(player))
|
||||
lambda state: upper_house_key(state, player))
|
||||
add_rule(world.get_location("New Muldul: Pot above Vault", player),
|
||||
lambda state: state._hylics2_can_air_dash(player))
|
||||
lambda state: air_dash(state, player))
|
||||
|
||||
# New Muldul Vault
|
||||
add_rule(world.get_location("New Muldul: Rescued Blerol 1", player),
|
||||
lambda state: ((state._hylics2_has_jail_key(player) and state._hylics2_has_paddle(player)) and\
|
||||
(state._hylics2_can_air_dash(player) or state._hylics2_has_airship(player))) or\
|
||||
state._hylics2_enter_hylemxylem(player))
|
||||
lambda state: (
|
||||
(
|
||||
(
|
||||
jail_key(state, player)
|
||||
and paddle(state, player)
|
||||
)
|
||||
and (
|
||||
air_dash(state, player)
|
||||
or airship(state, player)
|
||||
)
|
||||
)
|
||||
or enter_hylemxylem(state, player)
|
||||
))
|
||||
add_rule(world.get_location("New Muldul: Rescued Blerol 2", player),
|
||||
lambda state: ((state._hylics2_has_jail_key(player) and state._hylics2_has_paddle(player)) and\
|
||||
(state._hylics2_can_air_dash(player) or state._hylics2_has_airship(player))) or\
|
||||
state._hylics2_enter_hylemxylem(player))
|
||||
lambda state: (
|
||||
(
|
||||
(
|
||||
jail_key(state, player)
|
||||
and paddle(state, player)
|
||||
)
|
||||
and (
|
||||
air_dash(state, player)
|
||||
or airship(state, player)
|
||||
)
|
||||
)
|
||||
or enter_hylemxylem(state, player)
|
||||
))
|
||||
add_rule(world.get_location("New Muldul: Vault Left Chest", player),
|
||||
lambda state: state._hylics2_enter_hylemxylem(player))
|
||||
lambda state: enter_hylemxylem(state, player))
|
||||
add_rule(world.get_location("New Muldul: Vault Right Chest", player),
|
||||
lambda state: state._hylics2_enter_hylemxylem(player))
|
||||
lambda state: enter_hylemxylem(state, player))
|
||||
add_rule(world.get_location("New Muldul: Vault Bomb", player),
|
||||
lambda state: state._hylics2_enter_hylemxylem(player))
|
||||
lambda state: enter_hylemxylem(state, player))
|
||||
|
||||
# Viewax's Edifice
|
||||
add_rule(world.get_location("Viewax's Edifice: Canopic Jar", player),
|
||||
lambda state: state._hylics2_has_paddle(player))
|
||||
lambda state: paddle(state, player))
|
||||
add_rule(world.get_location("Viewax's Edifice: Cave Sarcophagus", player),
|
||||
lambda state: state._hylics2_has_paddle(player))
|
||||
lambda state: paddle(state, player))
|
||||
add_rule(world.get_location("Viewax's Edifice: Shielded Key", player),
|
||||
lambda state: state._hylics2_has_paddle(player))
|
||||
lambda state: paddle(state, player))
|
||||
add_rule(world.get_location("Viewax's Edifice: Shielded Key", player),
|
||||
lambda state: state._hylics2_has_paddle(player))
|
||||
lambda state: paddle(state, player))
|
||||
add_rule(world.get_location("Viewax's Edifice: Tower Pot", player),
|
||||
lambda state: state._hylics2_has_paddle(player))
|
||||
lambda state: paddle(state, player))
|
||||
add_rule(world.get_location("Viewax's Edifice: Tower Jar", player),
|
||||
lambda state: state._hylics2_has_paddle(player))
|
||||
lambda state: paddle(state, player))
|
||||
add_rule(world.get_location("Viewax's Edifice: Tower Chest", player),
|
||||
lambda state: state._hylics2_has_paddle(player) and state._hylics2_has_tower_key(player))
|
||||
lambda state: (
|
||||
paddle(state, player)
|
||||
and tower_key(state, player)
|
||||
))
|
||||
add_rule(world.get_location("Viewax's Edifice: Viewax Pot", player),
|
||||
lambda state: state._hylics2_has_paddle(player))
|
||||
lambda state: paddle(state, player))
|
||||
add_rule(world.get_location("Viewax's Edifice: Defeat Viewax", player),
|
||||
lambda state: state._hylics2_has_paddle(player))
|
||||
lambda state: paddle(state, player))
|
||||
add_rule(world.get_location("Viewax's Edifice: TV", player),
|
||||
lambda state: state._hylics2_has_paddle(player) and state._hylics2_has_jail_key(player))
|
||||
lambda state: (
|
||||
paddle(state, player)
|
||||
and jail_key(state, player)
|
||||
))
|
||||
add_rule(world.get_location("Viewax's Edifice: Sage Fridge", player),
|
||||
lambda state: state._hylics2_can_air_dash(player))
|
||||
lambda state: air_dash(state, player))
|
||||
add_rule(world.get_location("Viewax's Edifice: Sage Item 1", player),
|
||||
lambda state: state._hylics2_can_air_dash(player))
|
||||
lambda state: air_dash(state, player))
|
||||
add_rule(world.get_location("Viewax's Edifice: Sage Item 2", player),
|
||||
lambda state: state._hylics2_can_air_dash(player))
|
||||
lambda state: air_dash(state, player))
|
||||
|
||||
# Arcade 1
|
||||
add_rule(world.get_location("Arcade 1: Key", player),
|
||||
lambda state: state._hylics2_has_paddle(player))
|
||||
lambda state: paddle(state, player))
|
||||
add_rule(world.get_location("Arcade 1: Coin Dash", player),
|
||||
lambda state: state._hylics2_has_paddle(player))
|
||||
lambda state: paddle(state, player))
|
||||
add_rule(world.get_location("Arcade 1: Burrito Alcove 1", player),
|
||||
lambda state: state._hylics2_has_paddle(player))
|
||||
lambda state: paddle(state, player))
|
||||
add_rule(world.get_location("Arcade 1: Burrito Alcove 2", player),
|
||||
lambda state: state._hylics2_has_paddle(player))
|
||||
lambda state: paddle(state, player))
|
||||
add_rule(world.get_location("Arcade 1: Behind Spikes Banana", player),
|
||||
lambda state: state._hylics2_has_paddle(player))
|
||||
lambda state: paddle(state, player))
|
||||
add_rule(world.get_location("Arcade 1: Pyramid Banana", player),
|
||||
lambda state: state._hylics2_has_paddle(player))
|
||||
lambda state: paddle(state, player))
|
||||
add_rule(world.get_location("Arcade 1: Moving Platforms Muscle Applique", player),
|
||||
lambda state: state._hylics2_has_paddle(player))
|
||||
lambda state: paddle(state, player))
|
||||
add_rule(world.get_location("Arcade 1: Bed Banana", player),
|
||||
lambda state: state._hylics2_has_paddle(player))
|
||||
lambda state: paddle(state, player))
|
||||
|
||||
# Airship
|
||||
add_rule(world.get_location("Airship: Talk to Somsnosa", player),
|
||||
lambda state: state._hylics2_has_worm_room_key(player))
|
||||
lambda state: worm_room_key(state, player))
|
||||
|
||||
# Foglast
|
||||
add_rule(world.get_location("Foglast: Underground Sarcophagus", player),
|
||||
lambda state: state._hylics2_can_air_dash(player))
|
||||
lambda state: air_dash(state, player))
|
||||
add_rule(world.get_location("Foglast: Shielded Key", player),
|
||||
lambda state: state._hylics2_can_air_dash(player))
|
||||
lambda state: air_dash(state, player))
|
||||
add_rule(world.get_location("Foglast: TV", player),
|
||||
lambda state: state._hylics2_can_air_dash(player) and state._hylics2_has_clicker(player))
|
||||
lambda state: (
|
||||
air_dash(state, player)
|
||||
and clicker(state, player)
|
||||
))
|
||||
add_rule(world.get_location("Foglast: Buy Clicker", player),
|
||||
lambda state: state._hylics2_can_air_dash(player))
|
||||
lambda state: air_dash(state, player))
|
||||
add_rule(world.get_location("Foglast: Shielded Chest", player),
|
||||
lambda state: state._hylics2_can_air_dash(player))
|
||||
lambda state: air_dash(state, player))
|
||||
add_rule(world.get_location("Foglast: Cave Fridge", player),
|
||||
lambda state: state._hylics2_can_air_dash(player))
|
||||
lambda state: air_dash(state, player))
|
||||
add_rule(world.get_location("Foglast: Roof Sarcophagus", player),
|
||||
lambda state: state._hylics2_can_air_dash(player) and state._hylics2_has_bridge_key(player))
|
||||
lambda state: (
|
||||
air_dash(state, player)
|
||||
and bridge_key(state, player)
|
||||
))
|
||||
add_rule(world.get_location("Foglast: Under Lair Sarcophagus 1", player),
|
||||
lambda state: state._hylics2_can_air_dash(player) and state._hylics2_has_bridge_key(player))
|
||||
lambda state: (
|
||||
air_dash(state, player)
|
||||
and bridge_key(state, player)
|
||||
))
|
||||
add_rule(world.get_location("Foglast: Under Lair Sarcophagus 2", player),
|
||||
lambda state: state._hylics2_can_air_dash(player) and state._hylics2_has_bridge_key(player))
|
||||
lambda state: (
|
||||
air_dash(state, player)
|
||||
and bridge_key(state, player)
|
||||
))
|
||||
add_rule(world.get_location("Foglast: Under Lair Sarcophagus 3", player),
|
||||
lambda state: state._hylics2_can_air_dash(player) and state._hylics2_has_bridge_key(player))
|
||||
lambda state: (
|
||||
air_dash(state, player)
|
||||
and bridge_key(state, player)
|
||||
))
|
||||
add_rule(world.get_location("Foglast: Sage Sarcophagus", player),
|
||||
lambda state: state._hylics2_can_air_dash(player) and state._hylics2_has_bridge_key(player))
|
||||
lambda state: (
|
||||
air_dash(state, player)
|
||||
and bridge_key(state, player)
|
||||
))
|
||||
add_rule(world.get_location("Foglast: Sage Item 1", player),
|
||||
lambda state: state._hylics2_can_air_dash(player) and state._hylics2_has_bridge_key(player))
|
||||
lambda state: (
|
||||
air_dash(state, player)
|
||||
and bridge_key(state, player)
|
||||
))
|
||||
add_rule(world.get_location("Foglast: Sage Item 2", player),
|
||||
lambda state: state._hylics2_can_air_dash(player) and state._hylics2_has_bridge_key(player))
|
||||
lambda state: (
|
||||
air_dash(state, player)
|
||||
and bridge_key(state, player)
|
||||
))
|
||||
|
||||
# Drill Castle
|
||||
add_rule(world.get_location("Drill Castle: Island Banana", player),
|
||||
lambda state: state._hylics2_can_air_dash(player))
|
||||
lambda state: air_dash(state, player))
|
||||
add_rule(world.get_location("Drill Castle: Island Pot", player),
|
||||
lambda state: state._hylics2_can_air_dash(player))
|
||||
lambda state: air_dash(state, player))
|
||||
add_rule(world.get_location("Drill Castle: Cave Sarcophagus", player),
|
||||
lambda state: state._hylics2_can_air_dash(player))
|
||||
lambda state: air_dash(state, player))
|
||||
add_rule(world.get_location("Drill Castle: TV", player),
|
||||
lambda state: state._hylics2_can_air_dash(player))
|
||||
lambda state: air_dash(state, player))
|
||||
|
||||
# Sage Labyrinth
|
||||
add_rule(world.get_location("Sage Labyrinth: Sage Item 1", player),
|
||||
lambda state: state._hylics2_has_deep_key(player))
|
||||
lambda state: deep_key(state, player))
|
||||
add_rule(world.get_location("Sage Labyrinth: Sage Item 2", player),
|
||||
lambda state: state._hylics2_has_deep_key(player))
|
||||
lambda state: deep_key(state, player))
|
||||
add_rule(world.get_location("Sage Labyrinth: Sage Left Arm", player),
|
||||
lambda state: state._hylics2_has_deep_key(player))
|
||||
lambda state: deep_key(state, player))
|
||||
add_rule(world.get_location("Sage Labyrinth: Sage Right Arm", player),
|
||||
lambda state: state._hylics2_has_deep_key(player))
|
||||
lambda state: deep_key(state, player))
|
||||
add_rule(world.get_location("Sage Labyrinth: Sage Left Leg", player),
|
||||
lambda state: state._hylics2_has_deep_key(player))
|
||||
lambda state: deep_key(state, player))
|
||||
add_rule(world.get_location("Sage Labyrinth: Sage Right Leg", player),
|
||||
lambda state: state._hylics2_has_deep_key(player))
|
||||
lambda state: deep_key(state, player))
|
||||
|
||||
# Sage Airship
|
||||
add_rule(world.get_location("Sage Airship: TV", player),
|
||||
lambda state: state._hylics2_has_tokens(player))
|
||||
lambda state: all_tokens(state, player))
|
||||
|
||||
# Hylemxylem
|
||||
add_rule(world.get_location("Hylemxylem: Upper Chamber Banana", player),
|
||||
lambda state: state._hylics2_has_upper_chamber_key(player))
|
||||
lambda state: upper_chamber_key(state, player))
|
||||
add_rule(world.get_location("Hylemxylem: Across Upper Reservoir Chest", player),
|
||||
lambda state: state._hylics2_has_upper_chamber_key(player))
|
||||
lambda state: upper_chamber_key(state, player))
|
||||
add_rule(world.get_location("Hylemxylem: Drained Lower Reservoir Chest", player),
|
||||
lambda state: state._hylics2_has_upper_chamber_key(player))
|
||||
lambda state: upper_chamber_key(state, player))
|
||||
add_rule(world.get_location("Hylemxylem: Drained Lower Reservoir Burrito 1", player),
|
||||
lambda state: state._hylics2_has_upper_chamber_key(player))
|
||||
lambda state: upper_chamber_key(state, player))
|
||||
add_rule(world.get_location("Hylemxylem: Drained Lower Reservoir Burrito 2", player),
|
||||
lambda state: state._hylics2_has_upper_chamber_key(player))
|
||||
lambda state: upper_chamber_key(state, player))
|
||||
add_rule(world.get_location("Hylemxylem: Lower Reservoir Hole Pot 1", player),
|
||||
lambda state: state._hylics2_has_upper_chamber_key(player))
|
||||
lambda state: upper_chamber_key(state, player))
|
||||
add_rule(world.get_location("Hylemxylem: Lower Reservoir Hole Pot 2", player),
|
||||
lambda state: state._hylics2_has_upper_chamber_key(player))
|
||||
lambda state: upper_chamber_key(state, player))
|
||||
add_rule(world.get_location("Hylemxylem: Lower Reservoir Hole Pot 3", player),
|
||||
lambda state: state._hylics2_has_upper_chamber_key(player))
|
||||
lambda state: upper_chamber_key(state, player))
|
||||
add_rule(world.get_location("Hylemxylem: Lower Reservoir Hole Sarcophagus", player),
|
||||
lambda state: state._hylics2_has_upper_chamber_key(player))
|
||||
lambda state: upper_chamber_key(state, player))
|
||||
add_rule(world.get_location("Hylemxylem: Drained Upper Reservoir Burrito 1", player),
|
||||
lambda state: state._hylics2_has_upper_chamber_key(player))
|
||||
lambda state: upper_chamber_key(state, player))
|
||||
add_rule(world.get_location("Hylemxylem: Drained Upper Reservoir Burrito 2", player),
|
||||
lambda state: state._hylics2_has_upper_chamber_key(player))
|
||||
lambda state: upper_chamber_key(state, player))
|
||||
add_rule(world.get_location("Hylemxylem: Drained Upper Reservoir Burrito 3", player),
|
||||
lambda state: state._hylics2_has_upper_chamber_key(player))
|
||||
lambda state: upper_chamber_key(state, player))
|
||||
add_rule(world.get_location("Hylemxylem: Upper Reservoir Hole Key", player),
|
||||
lambda state: state._hylics2_has_upper_chamber_key(player))
|
||||
lambda state: upper_chamber_key(state, player))
|
||||
|
||||
# extra rules if Extra Items in Logic is enabled
|
||||
if world.extra_items_in_logic[player]:
|
||||
for i in world.get_region("Foglast", player).entrances:
|
||||
add_rule(i, lambda state: state._hylics2_has_charge_up(player))
|
||||
add_rule(i, lambda state: charge_up(state, player))
|
||||
for i in world.get_region("Sage Airship", player).entrances:
|
||||
add_rule(i, lambda state: state._hylics2_has_charge_up(player) and state._hylics2_has_cup(player) and\
|
||||
state._hylics2_has_worm_room_key(player))
|
||||
add_rule(i, lambda state: (
|
||||
charge_up(state, player)
|
||||
and paper_cup(state, player)
|
||||
and worm_room_key(state, player)
|
||||
))
|
||||
for i in world.get_region("Hylemxylem", player).entrances:
|
||||
add_rule(i, lambda state: state._hylics2_has_charge_up(player) and state._hylics2_has_cup(player))
|
||||
add_rule(i, lambda state: (
|
||||
charge_up(state, player)
|
||||
and paper_cup(state, player)
|
||||
))
|
||||
|
||||
add_rule(world.get_location("Sage Labyrinth: Motor Hunter Sarcophagus", player),
|
||||
lambda state: state._hylics2_has_charge_up(player) and state._hylics2_has_cup(player))
|
||||
lambda state: (
|
||||
charge_up(state, player)
|
||||
and paper_cup(state, player)
|
||||
))
|
||||
|
||||
# extra rules if Shuffle Party Members is enabled
|
||||
if world.party_shuffle[player]:
|
||||
for i in world.get_region("Arcade Island", player).entrances:
|
||||
add_rule(i, lambda state: state._hylics2_has_3_members(player))
|
||||
add_rule(i, lambda state: party_3(state, player))
|
||||
for i in world.get_region("Foglast", player).entrances:
|
||||
add_rule(i, lambda state: state._hylics2_has_3_members(player) or\
|
||||
(state._hylics2_has_2_members(player) and state._hylics2_has_jail_key(player)))
|
||||
add_rule(i, lambda state: (
|
||||
party_3(state, player)
|
||||
or (
|
||||
party_2(state, player)
|
||||
and jail_key(state, player)
|
||||
)
|
||||
))
|
||||
for i in world.get_region("Sage Airship", player).entrances:
|
||||
add_rule(i, lambda state: state._hylics2_has_3_members(player))
|
||||
add_rule(i, lambda state: party_3(state, player))
|
||||
for i in world.get_region("Hylemxylem", player).entrances:
|
||||
add_rule(i, lambda state: state._hylics2_has_3_members(player))
|
||||
add_rule(i, lambda state: party_3(state, player))
|
||||
|
||||
add_rule(world.get_location("Viewax's Edifice: Defeat Viewax", player),
|
||||
lambda state: state._hylics2_has_2_members(player))
|
||||
lambda state: party_2(state, player))
|
||||
add_rule(world.get_location("New Muldul: Rescued Blerol 1", player),
|
||||
lambda state: state._hylics2_has_2_members(player))
|
||||
lambda state: party_2(state, player))
|
||||
add_rule(world.get_location("New Muldul: Rescued Blerol 2", player),
|
||||
lambda state: state._hylics2_has_2_members(player))
|
||||
lambda state: party_2(state, player))
|
||||
add_rule(world.get_location("New Muldul: Vault Left Chest", player),
|
||||
lambda state: state._hylics2_has_3_members(player))
|
||||
lambda state: party_3(state, player))
|
||||
add_rule(world.get_location("New Muldul: Vault Right Chest", player),
|
||||
lambda state: state._hylics2_has_3_members(player))
|
||||
lambda state: party_3(state, player))
|
||||
add_rule(world.get_location("New Muldul: Vault Bomb", player),
|
||||
lambda state: state._hylics2_has_3_members(player))
|
||||
lambda state: party_3(state, player))
|
||||
add_rule(world.get_location("Juice Ranch: Battle with Somsnosa", player),
|
||||
lambda state: state._hylics2_has_2_members(player))
|
||||
lambda state: party_2(state, player))
|
||||
add_rule(world.get_location("Juice Ranch: Somsnosa Joins", player),
|
||||
lambda state: state._hylics2_has_2_members(player))
|
||||
lambda state: party_2(state, player))
|
||||
add_rule(world.get_location("Airship: Talk to Somsnosa", player),
|
||||
lambda state: state._hylics2_has_3_members(player))
|
||||
lambda state: party_3(state, player))
|
||||
add_rule(world.get_location("Sage Labyrinth: Motor Hunter Sarcophagus", player),
|
||||
lambda state: state._hylics2_has_3_members(player))
|
||||
lambda state: party_3(state, player))
|
||||
|
||||
# extra rules if Shuffle Red Medallions is enabled
|
||||
if world.medallion_shuffle[player]:
|
||||
add_rule(world.get_location("New Muldul: Upper House Medallion", player),
|
||||
lambda state: state._hylics2_has_upper_house_key(player))
|
||||
lambda state: upper_house_key(state, player))
|
||||
add_rule(world.get_location("New Muldul: Vault Rear Left Medallion", player),
|
||||
lambda state: state._hylics2_enter_foglast(player) and state._hylics2_has_bridge_key(player))
|
||||
lambda state: (
|
||||
enter_foglast(state, player)
|
||||
and bridge_key(state, player)
|
||||
))
|
||||
add_rule(world.get_location("New Muldul: Vault Rear Right Medallion", player),
|
||||
lambda state: state._hylics2_enter_foglast(player) and state._hylics2_has_bridge_key(player))
|
||||
lambda state: (
|
||||
enter_foglast(state, player)
|
||||
and bridge_key(state, player)
|
||||
))
|
||||
add_rule(world.get_location("New Muldul: Vault Center Medallion", player),
|
||||
lambda state: state._hylics2_enter_foglast(player) and state._hylics2_has_bridge_key(player))
|
||||
lambda state: (
|
||||
enter_foglast(state, player)
|
||||
and bridge_key(state, player)
|
||||
))
|
||||
add_rule(world.get_location("New Muldul: Vault Front Left Medallion", player),
|
||||
lambda state: state._hylics2_enter_foglast(player) and state._hylics2_has_bridge_key(player))
|
||||
lambda state: (
|
||||
enter_foglast(state, player)
|
||||
and bridge_key(state, player)
|
||||
))
|
||||
add_rule(world.get_location("New Muldul: Vault Front Right Medallion", player),
|
||||
lambda state: state._hylics2_enter_foglast(player) and state._hylics2_has_bridge_key(player))
|
||||
lambda state: (
|
||||
enter_foglast(state, player)
|
||||
and bridge_key(state, player)
|
||||
))
|
||||
add_rule(world.get_location("Viewax's Edifice: Fort Wall Medallion", player),
|
||||
lambda state: state._hylics2_has_paddle(player))
|
||||
lambda state: paddle(state, player))
|
||||
add_rule(world.get_location("Viewax's Edifice: Jar Medallion", player),
|
||||
lambda state: state._hylics2_has_paddle(player))
|
||||
lambda state: paddle(state, player))
|
||||
add_rule(world.get_location("Viewax's Edifice: Sage Chair Medallion", player),
|
||||
lambda state: state._hylics2_can_air_dash(player))
|
||||
lambda state: air_dash(state, player))
|
||||
add_rule(world.get_location("Arcade 1: Lonely Medallion", player),
|
||||
lambda state: state._hylics2_has_paddle(player))
|
||||
lambda state: paddle(state, player))
|
||||
add_rule(world.get_location("Arcade 1: Alcove Medallion", player),
|
||||
lambda state: state._hylics2_has_paddle(player))
|
||||
lambda state: paddle(state, player))
|
||||
add_rule(world.get_location("Foglast: Under Lair Medallion", player),
|
||||
lambda state: state._hylics2_has_bridge_key(player))
|
||||
lambda state: bridge_key(state, player))
|
||||
add_rule(world.get_location("Foglast: Mid-Air Medallion", player),
|
||||
lambda state: state._hylics2_can_air_dash(player))
|
||||
lambda state: air_dash(state, player))
|
||||
add_rule(world.get_location("Foglast: Top of Tower Medallion", player),
|
||||
lambda state: state._hylics2_has_paddle(player))
|
||||
lambda state: paddle(state, player))
|
||||
add_rule(world.get_location("Hylemxylem: Lower Reservoir Hole Medallion", player),
|
||||
lambda state: state._hylics2_has_upper_chamber_key(player))
|
||||
lambda state: upper_chamber_key(state, player))
|
||||
|
||||
# extra rules is Shuffle Red Medallions and Party Shuffle are enabled
|
||||
# extra rules if Shuffle Red Medallions and Party Shuffle are enabled
|
||||
if world.party_shuffle[player] and world.medallion_shuffle[player]:
|
||||
add_rule(world.get_location("New Muldul: Vault Rear Left Medallion", player),
|
||||
lambda state: state._hylics2_has_3_members(player))
|
||||
lambda state: party_3(state, player))
|
||||
add_rule(world.get_location("New Muldul: Vault Rear Right Medallion", player),
|
||||
lambda state: state._hylics2_has_3_members(player))
|
||||
lambda state: party_3(state, player))
|
||||
add_rule(world.get_location("New Muldul: Vault Center Medallion", player),
|
||||
lambda state: state._hylics2_has_3_members(player))
|
||||
lambda state: party_3(state, player))
|
||||
add_rule(world.get_location("New Muldul: Vault Front Left Medallion", player),
|
||||
lambda state: state._hylics2_has_3_members(player))
|
||||
lambda state: party_3(state, player))
|
||||
add_rule(world.get_location("New Muldul: Vault Front Right Medallion", player),
|
||||
lambda state: state._hylics2_has_3_members(player))
|
||||
lambda state: party_3(state, player))
|
||||
|
||||
# entrances
|
||||
for i in world.get_region("Airship", player).entrances:
|
||||
add_rule(i, lambda state: state._hylics2_has_airship(player))
|
||||
add_rule(i, lambda state: airship(state, player))
|
||||
for i in world.get_region("Arcade Island", player).entrances:
|
||||
add_rule(i, lambda state: state._hylics2_has_airship(player) and state._hylics2_can_air_dash(player))
|
||||
add_rule(i, lambda state: (
|
||||
airship(state, player)
|
||||
and air_dash(state, player)
|
||||
))
|
||||
for i in world.get_region("Worm Pod", player).entrances:
|
||||
add_rule(i, lambda state: state._hylics2_enter_wormpod(player))
|
||||
add_rule(i, lambda state: enter_wormpod(state, player))
|
||||
for i in world.get_region("Foglast", player).entrances:
|
||||
add_rule(i, lambda state: state._hylics2_enter_foglast(player))
|
||||
add_rule(i, lambda state: enter_foglast(state, player))
|
||||
for i in world.get_region("Sage Labyrinth", player).entrances:
|
||||
add_rule(i, lambda state: state._hylics2_has_skull_bomb(player))
|
||||
add_rule(i, lambda state: skull_bomb(state, player))
|
||||
for i in world.get_region("Sage Airship", player).entrances:
|
||||
add_rule(i, lambda state: state._hylics2_enter_sageship(player))
|
||||
add_rule(i, lambda state: enter_sageship(state, player))
|
||||
for i in world.get_region("Hylemxylem", player).entrances:
|
||||
add_rule(i, lambda state: state._hylics2_enter_hylemxylem(player))
|
||||
add_rule(i, lambda state: enter_hylemxylem(state, player))
|
||||
|
||||
# random start logic (default)
|
||||
if ((not world.random_start[player]) or \
|
||||
(world.random_start[player] and hylics2world.start_location == "Waynehouse")):
|
||||
# entrances
|
||||
for i in world.get_region("Viewax", player).entrances:
|
||||
add_rule(i, lambda state: state._hylics2_can_air_dash(player) or state._hylics2_has_airship(player))
|
||||
add_rule(i, lambda state: (
|
||||
air_dash(state, player)
|
||||
and airship(state, player)
|
||||
))
|
||||
for i in world.get_region("TV Island", player).entrances:
|
||||
add_rule(i, lambda state: state._hylics2_has_airship(player))
|
||||
add_rule(i, lambda state: airship(state, player))
|
||||
for i in world.get_region("Shield Facility", player).entrances:
|
||||
add_rule(i, lambda state: state._hylics2_has_airship(player))
|
||||
add_rule(i, lambda state: airship(state, player))
|
||||
for i in world.get_region("Juice Ranch", player).entrances:
|
||||
add_rule(i, lambda state: state._hylics2_has_airship(player))
|
||||
add_rule(i, lambda state: airship(state, player))
|
||||
|
||||
# random start logic (Viewax's Edifice)
|
||||
elif (world.random_start[player] and hylics2world.start_location == "Viewax's Edifice"):
|
||||
for i in world.get_region("Waynehouse", player).entrances:
|
||||
add_rule(i, lambda state: state._hylics2_can_air_dash(player) or state._hylics2_has_airship(player))
|
||||
add_rule(i, lambda state: (
|
||||
air_dash(state, player)
|
||||
or airship(state, player)
|
||||
))
|
||||
for i in world.get_region("New Muldul", player).entrances:
|
||||
add_rule(i, lambda state: state._hylics2_can_air_dash(player) or state._hylics2_has_airship(player))
|
||||
add_rule(i, lambda state: (
|
||||
air_dash(state, player)
|
||||
or airship(state, player)
|
||||
))
|
||||
for i in world.get_region("New Muldul Vault", player).entrances:
|
||||
add_rule(i, lambda state: state._hylics2_can_air_dash(player) or state._hylics2_has_airship(player))
|
||||
add_rule(i, lambda state: (
|
||||
air_dash(state, player)
|
||||
or airship(state, player)
|
||||
))
|
||||
for i in world.get_region("Drill Castle", player).entrances:
|
||||
add_rule(i, lambda state: state._hylics2_can_air_dash(player) or state._hylics2_has_airship(player))
|
||||
add_rule(i, lambda state: (
|
||||
air_dash(state, player)
|
||||
or airship(state, player)
|
||||
))
|
||||
for i in world.get_region("TV Island", player).entrances:
|
||||
add_rule(i, lambda state: state._hylics2_has_airship(player))
|
||||
add_rule(i, lambda state: airship(state, player))
|
||||
for i in world.get_region("Shield Facility", player).entrances:
|
||||
add_rule(i, lambda state: state._hylics2_has_airship(player))
|
||||
add_rule(i, lambda state: airship(state, player))
|
||||
for i in world.get_region("Juice Ranch", player).entrances:
|
||||
add_rule(i, lambda state: state._hylics2_has_airship(player))
|
||||
add_rule(i, lambda state: airship(state, player))
|
||||
for i in world.get_region("Sage Labyrinth", player).entrances:
|
||||
add_rule(i, lambda state: state._hylics2_has_airship(player))
|
||||
add_rule(i, lambda state: airship(state, player))
|
||||
|
||||
# random start logic (TV Island)
|
||||
elif (world.random_start[player] and hylics2world.start_location == "TV Island"):
|
||||
for i in world.get_region("Waynehouse", player).entrances:
|
||||
add_rule(i, lambda state: state._hylics2_has_airship(player))
|
||||
add_rule(i, lambda state: airship(state, player))
|
||||
for i in world.get_region("New Muldul", player).entrances:
|
||||
add_rule(i, lambda state: state._hylics2_has_airship(player))
|
||||
add_rule(i, lambda state: airship(state, player))
|
||||
for i in world.get_region("New Muldul Vault", player).entrances:
|
||||
add_rule(i, lambda state: state._hylics2_has_airship(player))
|
||||
add_rule(i, lambda state: airship(state, player))
|
||||
for i in world.get_region("Drill Castle", player).entrances:
|
||||
add_rule(i, lambda state: state._hylics2_has_airship(player))
|
||||
add_rule(i, lambda state: airship(state, player))
|
||||
for i in world.get_region("Viewax", player).entrances:
|
||||
add_rule(i, lambda state: state._hylics2_has_airship(player))
|
||||
add_rule(i, lambda state: airship(state, player))
|
||||
for i in world.get_region("Shield Facility", player).entrances:
|
||||
add_rule(i, lambda state: state._hylics2_has_airship(player))
|
||||
add_rule(i, lambda state: airship(state, player))
|
||||
for i in world.get_region("Juice Ranch", player).entrances:
|
||||
add_rule(i, lambda state: state._hylics2_has_airship(player))
|
||||
add_rule(i, lambda state: airship(state, player))
|
||||
for i in world.get_region("Sage Labyrinth", player).entrances:
|
||||
add_rule(i, lambda state: state._hylics2_has_airship(player))
|
||||
add_rule(i, lambda state: airship(state, player))
|
||||
|
||||
# random start logic (Shield Facility)
|
||||
elif (world.random_start[player] and hylics2world.start_location == "Shield Facility"):
|
||||
for i in world.get_region("Waynehouse", player).entrances:
|
||||
add_rule(i, lambda state: state._hylics2_has_airship(player))
|
||||
add_rule(i, lambda state: airship(state, player))
|
||||
for i in world.get_region("New Muldul", player).entrances:
|
||||
add_rule(i, lambda state: state._hylics2_has_airship(player))
|
||||
add_rule(i, lambda state: airship(state, player))
|
||||
for i in world.get_region("New Muldul Vault", player).entrances:
|
||||
add_rule(i, lambda state: state._hylics2_has_airship(player))
|
||||
add_rule(i, lambda state: airship(state, player))
|
||||
for i in world.get_region("Drill Castle", player).entrances:
|
||||
add_rule(i, lambda state: state._hylics2_has_airship(player))
|
||||
add_rule(i, lambda state: airship(state, player))
|
||||
for i in world.get_region("Viewax", player).entrances:
|
||||
add_rule(i, lambda state: state._hylics2_has_airship(player))
|
||||
add_rule(i, lambda state: airship(state, player))
|
||||
for i in world.get_region("TV Island", player).entrances:
|
||||
add_rule(i, lambda state: state._hylics2_has_airship(player))
|
||||
add_rule(i, lambda state: airship(state, player))
|
||||
for i in world.get_region("Sage Labyrinth", player).entrances:
|
||||
add_rule(i, lambda state: state._hylics2_has_airship(player))
|
||||
add_rule(i, lambda state: airship(state, player))
|
||||
@@ -130,11 +130,11 @@ class Hylics2World(World):
|
||||
tvs = list(Locations.tv_location_table.items())
|
||||
|
||||
# if Extra Items in Logic is enabled place CHARGE UP first and make sure it doesn't get
|
||||
# placed at Sage Airship: TV
|
||||
# placed at Sage Airship: TV or Foglast: TV
|
||||
if self.multiworld.extra_items_in_logic[self.player]:
|
||||
tv = self.multiworld.random.choice(tvs)
|
||||
gest = gestures.index((200681, Items.gesture_item_table[200681]))
|
||||
while tv[1]["name"] == "Sage Airship: TV":
|
||||
while tv[1]["name"] == "Sage Airship: TV" or tv[1]["name"] == "Foglast: TV":
|
||||
tv = self.multiworld.random.choice(tvs)
|
||||
self.multiworld.get_location(tv[1]["name"], self.player)\
|
||||
.place_locked_item(self.add_item(gestures[gest][1]["name"], gestures[gest][1]["classification"],
|
||||
@@ -146,7 +146,7 @@ class Hylics2World(World):
|
||||
gest = self.multiworld.random.choice(gestures)
|
||||
tv = self.multiworld.random.choice(tvs)
|
||||
self.multiworld.get_location(tv[1]["name"], self.player)\
|
||||
.place_locked_item(self.add_item(gest[1]["name"], gest[1]["classification"], gest[1]))
|
||||
.place_locked_item(self.add_item(gest[1]["name"], gest[1]["classification"], gest[0]))
|
||||
gestures.remove(gest)
|
||||
tvs.remove(tv)
|
||||
|
||||
@@ -232,8 +232,10 @@ class Hylics2World(World):
|
||||
# create location for beating the game and place Victory event there
|
||||
loc = Location(self.player, "Defeat Gibby", None, self.multiworld.get_region("Hylemxylem", self.player))
|
||||
loc.place_locked_item(self.create_event("Victory"))
|
||||
set_rule(loc, lambda state: state._hylics2_has_upper_chamber_key(self.player)
|
||||
and state._hylics2_has_vessel_room_key(self.player))
|
||||
set_rule(loc, lambda state: (
|
||||
state.has("UPPER CHAMBER KEY", self.player)
|
||||
and state.has("VESSEL ROOM KEY", self.player)
|
||||
))
|
||||
self.multiworld.get_region("Hylemxylem", self.player).locations.append(loc)
|
||||
self.multiworld.completion_condition[self.player] = lambda state: state.has("Victory", self.player)
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ your personal settings and export a config file from them.
|
||||
### Verifying your config file
|
||||
|
||||
If you would like to validate your config file to make sure it works, you may do so on the
|
||||
[YAML Validator](/mysterycheck) page.
|
||||
[YAML Validator](/check) page.
|
||||
|
||||
## Generating a Single-Player Game
|
||||
|
||||
|
||||
@@ -2,9 +2,8 @@ from enum import auto, Enum
|
||||
from typing import Dict, NamedTuple, Optional
|
||||
|
||||
from BaseClasses import Item, ItemClassification
|
||||
from . import Locations
|
||||
|
||||
start_id: int = Locations.start_id
|
||||
start_id: int = 0xAC0000
|
||||
|
||||
|
||||
class ItemType(Enum):
|
||||
@@ -500,7 +499,7 @@ l2ac_item_table: Dict[str, ItemData] = {
|
||||
# 0x01C8: "Key28"
|
||||
# 0x01C9: "Key29"
|
||||
# 0x01CA: "AP item" # replaces "Key30"
|
||||
# 0x01CB: "Crown"
|
||||
# 0x01CB: "SOLD OUT" # replaces "Crown"
|
||||
# 0x01CC: "Ruby apple"
|
||||
# 0x01CD: "PURIFIA"
|
||||
# 0x01CE: "Tag ring"
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import functools
|
||||
import numbers
|
||||
import random
|
||||
from dataclasses import dataclass
|
||||
from itertools import accumulate, chain, combinations
|
||||
from typing import Any, cast, Dict, Iterator, List, Mapping, Optional, Set, Tuple, Type, TYPE_CHECKING, Union
|
||||
|
||||
from Options import AssembleOptions, Choice, DeathLink, ItemDict, Range, SpecialRange, TextChoice, Toggle
|
||||
from Options import AssembleOptions, Choice, DeathLink, ItemDict, OptionDict, PerGameCommonOptions, Range, \
|
||||
SpecialRange, TextChoice, Toggle
|
||||
from .Enemies import enemy_name_to_sprite
|
||||
from .Items import ItemType, l2ac_item_table
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from BaseClasses import PlandoOptions
|
||||
@@ -557,6 +561,25 @@ class Goal(Choice):
|
||||
default = option_boss
|
||||
|
||||
|
||||
class GoldModifier(Range):
|
||||
"""Percentage modifier for gold gained from enemies.
|
||||
|
||||
Supported values: 25 – 400
|
||||
Default value: 100 (same as in an unmodified game)
|
||||
"""
|
||||
|
||||
display_name = "Gold modifier"
|
||||
range_start = 25
|
||||
range_end = 400
|
||||
default = 100
|
||||
|
||||
def __call__(self, gold: bytes) -> bytes:
|
||||
try:
|
||||
return (int.from_bytes(gold, "little") * self.value // 100).to_bytes(2, "little")
|
||||
except OverflowError:
|
||||
return b"\xFF\xFF"
|
||||
|
||||
|
||||
class HealingFloorChance(Range):
|
||||
"""The chance of a floor having a healing tile hidden under a bush.
|
||||
|
||||
@@ -661,6 +684,105 @@ class RunSpeed(Choice):
|
||||
default = option_disabled
|
||||
|
||||
|
||||
class ShopInterval(SpecialRange):
|
||||
"""Place shops after a certain number of floors.
|
||||
|
||||
E.g., if you set this to 5, then you will be given the opportunity to shop after completing B5, B10, B15, etc.,
|
||||
whereas if you set it to 1, then there will be a shop after every single completed floor.
|
||||
Shops will offer a random selection of wares; on deeper floors, more expensive items might appear.
|
||||
You can customize the stock that can appear in shops using the shop_inventory option.
|
||||
You can control how much gold you will be obtaining from enemies using the gold_multiplier option.
|
||||
Supported values: disabled, 1 – 10
|
||||
Default value: disabled (same as in an unmodified game)
|
||||
"""
|
||||
|
||||
display_name = "Shop interval"
|
||||
range_start = 0
|
||||
range_end = 10
|
||||
default = 0
|
||||
special_range_cutoff = 1
|
||||
special_range_names = {
|
||||
"disabled": 0,
|
||||
}
|
||||
|
||||
|
||||
class ShopInventory(OptionDict):
|
||||
"""Determine the item types that can appear in shops.
|
||||
|
||||
The value of this option should be a mapping of item categories (or individual items) to weights (non-negative
|
||||
integers), which are used as relative probabilities when it comes to including these things in shops. (The actual
|
||||
contents of the generated shops are selected randomly and are subject to additional constraints such as more
|
||||
expensive things being allowed only on later floors.)
|
||||
Supported keys:
|
||||
non_restorative — a selection of mostly non-restorative red chest consumables
|
||||
restorative — all HP- or MP-restoring red chest consumables
|
||||
blue_chest — all blue chest items
|
||||
spell — all red chest spells
|
||||
gear — all red chest armors, shields, headgear, rings, and rocks (this respects the gear_variety_after_b9 option,
|
||||
meaning that you will not encounter any shields, headgear, rings, or rocks in shops from B10 onward unless you
|
||||
also enabled that option)
|
||||
weapon — all red chest weapons
|
||||
Additionally, you can also add extra weights for any specific cave item. If you want your shops to have a
|
||||
higher than normal chance of selling a Dekar blade, you can, e.g., add "Dekar blade: 5".
|
||||
You can even forego the predefined categories entirely and design a custom shop pool from scratch by providing
|
||||
separate weights for each item you want to include.
|
||||
(Spells, however, cannot be weighted individually and are only available as part of the "spell" category.)
|
||||
Default value: {spell: 30, gear: 45, weapon: 82}
|
||||
"""
|
||||
|
||||
display_name = "Shop inventory"
|
||||
_special_keys = {"non_restorative", "restorative", "blue_chest", "spell", "gear", "weapon"}
|
||||
valid_keys = _special_keys | {item for item, data in l2ac_item_table.items()
|
||||
if data.type in {ItemType.BLUE_CHEST, ItemType.ENEMY_DROP, ItemType.ENTRANCE_CHEST,
|
||||
ItemType.RED_CHEST, ItemType.RED_CHEST_PATCH}}
|
||||
default: Dict[str, int] = {
|
||||
"spell": 30,
|
||||
"gear": 45,
|
||||
"weapon": 82,
|
||||
}
|
||||
value: Dict[str, int]
|
||||
|
||||
def verify(self, world: Type[World], player_name: str, plando_options: PlandoOptions) -> None:
|
||||
super().verify(world, player_name, plando_options)
|
||||
for item, weight in self.value.items():
|
||||
if not isinstance(weight, numbers.Integral) or weight < 0:
|
||||
raise Exception(f"Weight for item \"{item}\" from option {self} must be a non-negative integer, "
|
||||
f"but was \"{weight}\".")
|
||||
|
||||
@property
|
||||
def total(self) -> int:
|
||||
return sum(self.value.values())
|
||||
|
||||
@property
|
||||
def non_restorative(self) -> int:
|
||||
return self.value.get("non_restorative", 0)
|
||||
|
||||
@property
|
||||
def restorative(self) -> int:
|
||||
return self.value.get("restorative", 0)
|
||||
|
||||
@property
|
||||
def blue_chest(self) -> int:
|
||||
return self.value.get("blue_chest", 0)
|
||||
|
||||
@property
|
||||
def spell(self) -> int:
|
||||
return self.value.get("spell", 0)
|
||||
|
||||
@property
|
||||
def gear(self) -> int:
|
||||
return self.value.get("gear", 0)
|
||||
|
||||
@property
|
||||
def weapon(self) -> int:
|
||||
return self.value.get("weapon", 0)
|
||||
|
||||
@functools.cached_property
|
||||
def custom(self) -> Dict[int, int]:
|
||||
return {l2ac_item_table[item].code & 0x01FF: weight for item, weight in self.value.items()
|
||||
if item not in self._special_keys}
|
||||
|
||||
|
||||
class ShuffleCapsuleMonsters(Toggle):
|
||||
"""Shuffle the capsule monsters into the multiworld.
|
||||
|
||||
@@ -697,7 +819,7 @@ class ShufflePartyMembers(Toggle):
|
||||
|
||||
|
||||
@dataclass
|
||||
class L2ACOptions:
|
||||
class L2ACOptions(PerGameCommonOptions):
|
||||
blue_chest_chance: BlueChestChance
|
||||
blue_chest_count: BlueChestCount
|
||||
boss: Boss
|
||||
@@ -716,6 +838,7 @@ class L2ACOptions:
|
||||
final_floor: FinalFloor
|
||||
gear_variety_after_b9: GearVarietyAfterB9
|
||||
goal: Goal
|
||||
gold_modifier: GoldModifier
|
||||
healing_floor_chance: HealingFloorChance
|
||||
initial_floor: InitialFloor
|
||||
iris_floor_chance: IrisFloorChance
|
||||
@@ -723,5 +846,7 @@ class L2ACOptions:
|
||||
master_hp: MasterHp
|
||||
party_starting_level: PartyStartingLevel
|
||||
run_speed: RunSpeed
|
||||
shop_interval: ShopInterval
|
||||
shop_inventory: ShopInventory
|
||||
shuffle_capsule_monsters: ShuffleCapsuleMonsters
|
||||
shuffle_party_members: ShufflePartyMembers
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import itertools
|
||||
from operator import itemgetter
|
||||
from random import Random
|
||||
from typing import Dict, List, MutableSequence, Sequence, Set, Tuple
|
||||
from typing import Dict, Iterable, List, MutableSequence, Sequence, Set, Tuple
|
||||
|
||||
|
||||
def constrained_choices(population: Sequence[int], d: int, *, k: int, random: Random) -> List[int]:
|
||||
@@ -19,3 +21,10 @@ def constrained_shuffle(x: MutableSequence[int], d: int, random: Random) -> None
|
||||
i, j = random.randrange(n), random.randrange(n)
|
||||
if x[i] in constraints[j] and x[j] in constraints[i]:
|
||||
x[i], x[j] = x[j], x[i]
|
||||
|
||||
|
||||
def weighted_sample(population: Iterable[int], weights: Iterable[float], k: int, *, random: Random) -> List[int]:
|
||||
population, keys = zip(*((item, pow(random.random(), 1 / group_weight))
|
||||
for item, group in itertools.groupby(sorted(zip(population, weights)), key=itemgetter(0))
|
||||
if (group_weight := sum(weight for _, weight in group))))
|
||||
return sorted(population, key=dict(zip(population, keys)).__getitem__)[-k:]
|
||||
|
||||
@@ -2,11 +2,11 @@ import base64
|
||||
import itertools
|
||||
import os
|
||||
from enum import IntFlag
|
||||
from typing import Any, ClassVar, Dict, get_type_hints, Iterator, List, Set, Tuple
|
||||
from typing import Any, ClassVar, Dict, Iterator, List, Set, Tuple, Type
|
||||
|
||||
import settings
|
||||
from BaseClasses import Item, ItemClassification, Location, MultiWorld, Region, Tutorial
|
||||
from Options import AssembleOptions
|
||||
from Options import PerGameCommonOptions
|
||||
from Utils import __version__
|
||||
from worlds.AutoWorld import WebWorld, World
|
||||
from worlds.generic.Rules import add_rule, set_rule
|
||||
@@ -14,9 +14,9 @@ from .Client import L2ACSNIClient # noqa: F401
|
||||
from .Items import ItemData, ItemType, l2ac_item_name_to_id, l2ac_item_table, L2ACItem, start_id as items_start_id
|
||||
from .Locations import l2ac_location_name_to_id, L2ACLocation
|
||||
from .Options import CapsuleStartingLevel, DefaultParty, EnemyFloorNumbers, EnemyMovementPatterns, EnemySprites, \
|
||||
ExpModifier, Goal, L2ACOptions
|
||||
Goal, L2ACOptions
|
||||
from .Rom import get_base_rom_bytes, get_base_rom_path, L2ACDeltaPatch
|
||||
from .Utils import constrained_choices, constrained_shuffle
|
||||
from .Utils import constrained_choices, constrained_shuffle, weighted_sample
|
||||
from .basepatch import apply_basepatch
|
||||
|
||||
CHESTS_PER_SPHERE: int = 5
|
||||
@@ -54,7 +54,8 @@ class L2ACWorld(World):
|
||||
game: ClassVar[str] = "Lufia II Ancient Cave"
|
||||
web: ClassVar[WebWorld] = L2ACWeb()
|
||||
|
||||
option_definitions: ClassVar[Dict[str, AssembleOptions]] = get_type_hints(L2ACOptions)
|
||||
options_dataclass: ClassVar[Type[PerGameCommonOptions]] = L2ACOptions
|
||||
options: L2ACOptions
|
||||
settings: ClassVar[L2ACSettings]
|
||||
item_name_to_id: ClassVar[Dict[str, int]] = l2ac_item_name_to_id
|
||||
location_name_to_id: ClassVar[Dict[str, int]] = l2ac_location_name_to_id
|
||||
@@ -87,7 +88,7 @@ class L2ACWorld(World):
|
||||
bytearray(f"L2AC{__version__.replace('.', '')[:3]}_{self.player}_{self.multiworld.seed}", "utf8")[:21]
|
||||
self.rom_name.extend([0] * (21 - len(self.rom_name)))
|
||||
|
||||
self.o = L2ACOptions(**{opt: getattr(self.multiworld, opt)[self.player] for opt in self.option_definitions})
|
||||
self.o = self.options
|
||||
|
||||
if self.o.blue_chest_count < self.o.custom_item_pool.count:
|
||||
raise ValueError(f"Number of items in custom_item_pool ({self.o.custom_item_pool.count}) is "
|
||||
@@ -221,6 +222,7 @@ class L2ACWorld(World):
|
||||
rom_bytearray[0x09D59B:0x09D59B + 256] = self.get_node_connection_table()
|
||||
rom_bytearray[0x0B05C0:0x0B05C0 + 18843] = self.get_enemy_stats()
|
||||
rom_bytearray[0x0B4F02:0x0B4F02 + 2] = self.o.master_hp.value.to_bytes(2, "little")
|
||||
rom_bytearray[0x0BEE9F:0x0BEE9F + 1948] = self.get_shops()
|
||||
rom_bytearray[0x280010:0x280010 + 2] = self.o.blue_chest_count.value.to_bytes(2, "little")
|
||||
rom_bytearray[0x280012:0x280012 + 3] = self.o.capsule_starting_level.xp.to_bytes(3, "little")
|
||||
rom_bytearray[0x280015:0x280015 + 1] = self.o.initial_floor.value.to_bytes(1, "little")
|
||||
@@ -228,6 +230,7 @@ class L2ACWorld(World):
|
||||
rom_bytearray[0x280017:0x280017 + 1] = self.o.iris_treasures_required.value.to_bytes(1, "little")
|
||||
rom_bytearray[0x280018:0x280018 + 1] = self.o.shuffle_party_members.unlock.to_bytes(1, "little")
|
||||
rom_bytearray[0x280019:0x280019 + 1] = self.o.shuffle_capsule_monsters.unlock.to_bytes(1, "little")
|
||||
rom_bytearray[0x28001A:0x28001A + 1] = self.o.shop_interval.value.to_bytes(1, "little")
|
||||
rom_bytearray[0x280030:0x280030 + 1] = self.o.goal.value.to_bytes(1, "little")
|
||||
rom_bytearray[0x28003D:0x28003D + 1] = self.o.death_link.value.to_bytes(1, "little")
|
||||
rom_bytearray[0x281200:0x281200 + 470] = self.get_capsule_cravings_table()
|
||||
@@ -356,7 +359,7 @@ class L2ACWorld(World):
|
||||
def get_enemy_stats(self) -> bytes:
|
||||
rom: bytes = get_base_rom_bytes()
|
||||
|
||||
if self.o.exp_modifier == ExpModifier.default:
|
||||
if self.o.exp_modifier == 100 and self.o.gold_modifier == 100:
|
||||
return rom[0x0B05C0:0x0B05C0 + 18843]
|
||||
|
||||
number_of_enemies: int = 224
|
||||
@@ -365,6 +368,7 @@ class L2ACWorld(World):
|
||||
for enemy_id in range(number_of_enemies):
|
||||
pointer: int = int.from_bytes(enemy_stats[2 * enemy_id:2 * enemy_id + 2], "little")
|
||||
enemy_stats[pointer + 29:pointer + 31] = self.o.exp_modifier(enemy_stats[pointer + 29:pointer + 31])
|
||||
enemy_stats[pointer + 31:pointer + 33] = self.o.gold_modifier(enemy_stats[pointer + 31:pointer + 33])
|
||||
return enemy_stats
|
||||
|
||||
def get_goal_text_bytes(self) -> bytes:
|
||||
@@ -382,6 +386,90 @@ class L2ACWorld(World):
|
||||
goal_text_bytes = bytes((0x08, *b"\x03".join(line.encode("ascii") for line in goal_text), 0x00))
|
||||
return goal_text_bytes + b"\x00" * (147 - len(goal_text_bytes))
|
||||
|
||||
def get_shops(self) -> bytes:
|
||||
rom: bytes = get_base_rom_bytes()
|
||||
|
||||
if not self.o.shop_interval:
|
||||
return rom[0x0BEE9F:0x0BEE9F + 1948]
|
||||
|
||||
non_restorative_ids = {int.from_bytes(rom[0x0A713D + 2 * i:0x0A713D + 2 * i + 2], "little") for i in range(31)}
|
||||
restorative_ids = {int.from_bytes(rom[0x08FFDC + 2 * i:0x08FFDC + 2 * i + 2], "little") for i in range(9)}
|
||||
blue_ids = {int.from_bytes(rom[0x0A6EA0 + 2 * i:0x0A6EA0 + 2 * i + 2], "little") for i in range(41)}
|
||||
number_of_spells: int = 35
|
||||
number_of_items: int = 467
|
||||
spells_offset: int = 0x0AFA5B
|
||||
items_offset: int = 0x0B4F69
|
||||
non_restorative_list: List[List[int]] = [list() for _ in range(99)]
|
||||
restorative_list: List[List[int]] = [list() for _ in range(99)]
|
||||
blue_list: List[List[int]] = [list() for _ in range(99)]
|
||||
spell_list: List[List[int]] = [list() for _ in range(99)]
|
||||
gear_list: List[List[int]] = [list() for _ in range(99)]
|
||||
weapon_list: List[List[int]] = [list() for _ in range(99)]
|
||||
custom_list: List[List[int]] = [list() for _ in range(99)]
|
||||
|
||||
for spell_id in range(number_of_spells):
|
||||
pointer: int = int.from_bytes(rom[spells_offset + 2 * spell_id:spells_offset + 2 * spell_id + 2], "little")
|
||||
value: int = int.from_bytes(rom[spells_offset + pointer + 15:spells_offset + pointer + 17], "little")
|
||||
for f in range(value // 1000, 99):
|
||||
spell_list[f].append(spell_id)
|
||||
for item_id in range(number_of_items):
|
||||
pointer = int.from_bytes(rom[items_offset + 2 * item_id:items_offset + 2 * item_id + 2], "little")
|
||||
buckets: List[List[List[int]]] = list()
|
||||
if item_id in non_restorative_ids:
|
||||
buckets.append(non_restorative_list)
|
||||
if item_id in restorative_ids:
|
||||
buckets.append(restorative_list)
|
||||
if item_id in blue_ids:
|
||||
buckets.append(blue_list)
|
||||
if not rom[items_offset + pointer] & 0x20 and not rom[items_offset + pointer + 1] & 0x20:
|
||||
category: int = rom[items_offset + pointer + 7]
|
||||
if category >= 0x02:
|
||||
buckets.append(gear_list)
|
||||
elif category == 0x01:
|
||||
buckets.append(weapon_list)
|
||||
if item_id in self.o.shop_inventory.custom:
|
||||
buckets.append(custom_list)
|
||||
value = int.from_bytes(rom[items_offset + pointer + 5:items_offset + pointer + 7], "little")
|
||||
for bucket in buckets:
|
||||
for f in range(value // 1000, 99):
|
||||
bucket[f].append(item_id)
|
||||
|
||||
if not self.o.gear_variety_after_b9:
|
||||
for f in range(99):
|
||||
del gear_list[f][len(gear_list[f]) % 128:]
|
||||
|
||||
def create_shop(floor: int) -> Tuple[int, ...]:
|
||||
if self.random.randrange(self.o.shop_inventory.total) < self.o.shop_inventory.spell:
|
||||
return create_spell_shop(floor)
|
||||
else:
|
||||
return create_item_shop(floor)
|
||||
|
||||
def create_spell_shop(floor: int) -> Tuple[int, ...]:
|
||||
spells = self.random.sample(spell_list[floor], 3)
|
||||
return 0x03, 0x20, 0x00, *spells, 0xFF
|
||||
|
||||
def create_item_shop(floor: int) -> Tuple[int, ...]:
|
||||
population = non_restorative_list[floor] + restorative_list[floor] + blue_list[floor] \
|
||||
+ gear_list[floor] + weapon_list[floor] + custom_list[floor]
|
||||
weights = itertools.chain(*([weight / len_] * len_ if (len_ := len(list_)) else [] for weight, list_ in
|
||||
[(self.o.shop_inventory.non_restorative, non_restorative_list[floor]),
|
||||
(self.o.shop_inventory.restorative, restorative_list[floor]),
|
||||
(self.o.shop_inventory.blue_chest, blue_list[floor]),
|
||||
(self.o.shop_inventory.gear, gear_list[floor]),
|
||||
(self.o.shop_inventory.weapon, weapon_list[floor])]),
|
||||
(self.o.shop_inventory.custom[item] for item in custom_list[floor]))
|
||||
items = weighted_sample(population, weights, 5, random=self.random)
|
||||
return 0x01, 0x04, 0x00, *(b for item in items for b in item.to_bytes(2, "little")), 0x00, 0x00
|
||||
|
||||
shops = [create_shop(floor)
|
||||
for floor in range(self.o.shop_interval, 99, self.o.shop_interval)
|
||||
for _ in range(self.o.shop_interval)]
|
||||
shop_pointers = itertools.accumulate((len(shop) for shop in shops[:-1]), initial=2 * len(shops))
|
||||
shop_bytes = bytes(itertools.chain(*(p.to_bytes(2, "little") for p in shop_pointers), *shops))
|
||||
|
||||
assert len(shop_bytes) <= 1948, shop_bytes
|
||||
return shop_bytes.ljust(1948, b"\x00")
|
||||
|
||||
@staticmethod
|
||||
def get_node_connection_table() -> bytes:
|
||||
class Connect(IntFlag):
|
||||
|
||||
@@ -71,6 +71,11 @@ org $9EDD60 ; name
|
||||
org $9FA900 ; sprite
|
||||
incbin "ap_logo/ap_logo.bin"
|
||||
warnpc $9FA980
|
||||
; sold out item
|
||||
org $96F9BA ; properties
|
||||
DB $00,$00,$00,$10,$00,$00,$00,$00,$00,$00,$00,$00,$00
|
||||
org $9EDD6C ; name
|
||||
DB "SOLD OUT " ; overwrites "Crown "
|
||||
|
||||
|
||||
org $D08000 ; signature, start of expanded data area
|
||||
@@ -825,6 +830,119 @@ SpellRNG:
|
||||
|
||||
|
||||
|
||||
; shops
|
||||
pushpc
|
||||
org $83B442
|
||||
; DB=$83, x=1, m=1
|
||||
JSL Shop ; overwrites STA $7FD0BF
|
||||
pullpc
|
||||
|
||||
Shop:
|
||||
STA $7FD0BF ; (overwritten instruction)
|
||||
LDY $05AC ; load map number
|
||||
CPY.b #$F0 ; check if ancient cave
|
||||
BCC +
|
||||
LDA $05B4 ; check if going to ancient cave entrance
|
||||
BEQ +
|
||||
LDA $7FE696 ; load next to next floor number
|
||||
DEC
|
||||
CPY.b #$F1 ; check if going to final floor
|
||||
BCS ++ ; skip a decrement because next floor number is not incremented on final floor
|
||||
DEC
|
||||
++: CMP $D08015 ; check if past initial floor
|
||||
BCC +
|
||||
STA $4204 ; WRDIVL; dividend = floor number
|
||||
STZ $4205 ; WRDIVH
|
||||
TAX
|
||||
LDA $D0801A
|
||||
STA $4206 ; WRDIVB; divisor = shop_interval
|
||||
STA $211C ; M7B; second factor = shop_interval
|
||||
JSL $8082C7 ; advance RNG (while waiting for division to complete)
|
||||
LDY $4216 ; RDMPYL; skip if remainder (i.e., floor number mod shop_interval) is not 0
|
||||
BNE +
|
||||
STA $211B
|
||||
STZ $211B ; M7A; first factor = random number from 0 to 255
|
||||
TXA
|
||||
CLC
|
||||
SBC $2135 ; MPYM; calculate (floor number) - (random number from 0 to shop_interval-1) - 1
|
||||
STA $30 ; set shop id
|
||||
STZ $05A8 ; initialize variable for sold out item tracking
|
||||
STZ $05A9
|
||||
PHB
|
||||
PHP
|
||||
JML $80A33A ; open shop menu
|
||||
+: RTL
|
||||
|
||||
; shop item select
|
||||
pushpc
|
||||
org $82DF50
|
||||
; DB=$83, x=0, m=1
|
||||
JML ShopItemSelected ; overwrites JSR $8B08 : CMP.b #$01
|
||||
pullpc
|
||||
|
||||
ShopItemSelected:
|
||||
LDA $1548 ; check inventory free space
|
||||
BEQ +
|
||||
JSR LoadShopSlotAsFlag
|
||||
BIT $05A8 ; test item not already sold
|
||||
BNE +
|
||||
JML $82DF79 ; skip quantity selection and go directly to buy/equip
|
||||
+: JML $82DF80 ; abort and go back to item selection
|
||||
|
||||
; track bought shop items
|
||||
pushpc
|
||||
org $82E084
|
||||
; DB=$83, x=0, m=1
|
||||
JSL ShopBuy ; overwrites LDA.b #$05 : LDX.w #$0007
|
||||
NOP
|
||||
org $82E10E
|
||||
; DB=$83, x=0, m=1
|
||||
JSL ShopEquip ; overwrites SEP #$10 : LDX $14DC
|
||||
NOP
|
||||
pullpc
|
||||
|
||||
ShopBuy:
|
||||
JSR LoadShopSlotAsFlag
|
||||
TSB $05A8 ; mark item as sold
|
||||
LDA.b #$05 ; (overwritten instruction)
|
||||
LDX.w #$0007 ; (overwritten instruction)
|
||||
RTL
|
||||
|
||||
ShopEquip:
|
||||
JSR LoadShopSlotAsFlag
|
||||
TSB $05A8 ; mark item as sold
|
||||
SEP #$10 ; (overwritten instruction)
|
||||
LDX $14DC ; (overwritten instruction)
|
||||
RTL
|
||||
|
||||
LoadShopSlotAsFlag:
|
||||
TDC
|
||||
LDA $14EC ; load currently selected shop slot number
|
||||
ASL
|
||||
TAX
|
||||
LDA $8ED8C3,X ; load predefined bitmask with a single bit set
|
||||
RTS
|
||||
|
||||
; mark bought items as sold out
|
||||
pushpc
|
||||
org $8285EA
|
||||
; DB=$83, x=0, m=0
|
||||
JSL SoldOut ; overwrites LDA [$FC],Y : AND #$01FF
|
||||
NOP
|
||||
pullpc
|
||||
|
||||
SoldOut:
|
||||
LDA $8ED8C3,X ; load predefined bitmask with a single bit set
|
||||
BIT $05A8 ; test sold items
|
||||
BEQ +
|
||||
LDA.w #$01CB ; load sold out item id
|
||||
BRA ++
|
||||
+: LDA [$FC],Y ; (overwritten instruction)
|
||||
AND #$01FF ; (overwritten instruction)
|
||||
++: RTL
|
||||
|
||||
|
||||
|
||||
; increase variety of red chest gear after B9
|
||||
pushpc
|
||||
org $839176
|
||||
@@ -1009,6 +1127,53 @@ pullpc
|
||||
|
||||
|
||||
|
||||
; door stairs fix
|
||||
pushpc
|
||||
org $839453
|
||||
; DB=$7F, x=0, m=1
|
||||
JSL DoorStairsFix ; overwrites JSR $9B18 : JSR $9D11
|
||||
NOP #2
|
||||
pullpc
|
||||
|
||||
DoorStairsFix:
|
||||
CLC
|
||||
LDY.w #$0000
|
||||
--: LDX.w #$00FF ; loop through floor layout starting from the bottom right
|
||||
-: LDA $EA00,X ; read node contents
|
||||
BEQ + ; always skip empty nodes
|
||||
BCC ++ ; 1st pass: skip all blocked nodes (would cause door stairs or rare stairs)
|
||||
LDA $E9F0,X ; 2nd pass: skip only if the one above is also blocked (would cause door stairs)
|
||||
++: BMI +
|
||||
INY ; count usable nodes
|
||||
+: DEX
|
||||
BPL -
|
||||
TYA
|
||||
BNE ++ ; all nodes blocked?
|
||||
SEC ; set up 2nd, less restrictive pass
|
||||
BRA --
|
||||
++: JSL $8082C7 ; advance RNG
|
||||
STA $00211B
|
||||
TDC
|
||||
STA $00211B ; M7A; first factor = random number from 0 to 255
|
||||
TYA
|
||||
STA $00211C ; M7B; second factor = number of possible stair positions
|
||||
LDA $002135 ; MPYM; calculate random number from 0 to number of possible stair positions - 1
|
||||
TAY
|
||||
LDX.w #$00FF ; loop through floor layout starting from the bottom right
|
||||
-: LDA $EA00,X ; read node contents
|
||||
BEQ + ; always skip empty nodes
|
||||
BCC ++ ; if 1st pass was sufficient: skip all blocked nodes (prevent door stairs and rare stairs)
|
||||
LDA $E9F0,X ; if 2nd pass was needed: skip only if the one above is also blocked (prevent door stairs)
|
||||
++: BMI +
|
||||
DEY ; count down to locate the (Y+1)th usable node
|
||||
BMI ++
|
||||
+: DEX
|
||||
BPL -
|
||||
++: TXA ; return selected stair node coordinate
|
||||
RTL
|
||||
|
||||
|
||||
|
||||
; equipment text fix
|
||||
pushpc
|
||||
org $81F2E3
|
||||
@@ -1054,6 +1219,7 @@ pullpc
|
||||
; $F02017 1 iris treasures required
|
||||
; $F02018 1 party members available
|
||||
; $F02019 1 capsule monsters available
|
||||
; $F0201A 1 shop interval
|
||||
; $F02030 1 selected goal
|
||||
; $F02031 1 goal completion: boss
|
||||
; $F02032 1 goal completion: iris_treasure_hunt
|
||||
|
||||