Merge branch 'main' into breaking_changes

# Conflicts:
#	BaseClasses.py
#	Mystery.py
#	WebHostLib/downloads.py
#	WebHostLib/models.py
#	WebHostLib/templates/macros.html
#	WebHostLib/upload.py
#	worlds/alttp/ItemPool.py
#	worlds/alttp/Main.py
This commit is contained in:
Fabian Dill 2021-01-17 06:50:25 +01:00
commit 514cd19367
35 changed files with 965 additions and 187 deletions

View File

@ -5,7 +5,7 @@ from enum import Enum, unique
import logging
import json
from collections import OrderedDict, Counter, deque
from typing import Union, Optional, List, Dict
from typing import *
import secrets
import random
@ -345,6 +345,12 @@ class MultiWorld():
return [location for location in self.get_locations() if
(player is None or location.player == player) and location.item is None and location.can_reach(state)]
def get_unfilled_locations_for_players(self, location_name: str, players: Iterable[int]):
for player in players:
location = self.get_location(location_name, player)
if location.item is None:
yield location
def unlocks_new_location(self, item) -> bool:
temp_state = self.state.copy()
temp_state.collect(item, True)
@ -1418,3 +1424,27 @@ class Spoiler(object):
outfile.write('\n'.join(path_listings))
class PlandoItem(NamedTuple):
item: str
location: str
world: Union[bool, str] = False # False -> own world, True -> not own world
from_pool: bool = True # if item should be removed from item pool
force: Union[bool, str] = 'silent' # False -> warns if item not successfully placed. True -> errors out on failure to place item.
def warn(self, warning: str):
if str(self.force).lower() in ['true', 'fail', 'failure', 'none', 'false', 'warn', 'warning']:
logging.warning(f'{warning}')
else:
logging.debug(f'{warning}')
def failed(self, warning: str, exception=Exception):
if str(self.force).lower() in ['true', 'fail', 'failure']:
raise exception(warning)
else:
self.warn(warning)
class PlandoConnection(NamedTuple):
entrance: str
exit: str
direction: str # entrance, exit or both

102
Fill.py
View File

@ -1,12 +1,15 @@
import logging
import typing
from BaseClasses import CollectionState
from BaseClasses import CollectionState, PlandoItem
from Items import ItemFactory
from Regions import key_drop_data
class FillError(RuntimeError):
pass
def fill_restrictive(world, base_state: CollectionState, locations, itempool, single_player_placement=False):
def sweep_from_pool():
new_state = base_state.copy()
@ -16,6 +19,7 @@ def fill_restrictive(world, base_state: CollectionState, locations, itempool, si
return new_state
unplaced_items = []
placements = []
no_access_checks = {}
reachable_items = {}
@ -49,11 +53,6 @@ def fill_restrictive(world, base_state: CollectionState, locations, itempool, si
logging.warning(
f'Not all items placed. Game beatable anyway. (Could not place {item_to_place})')
continue
placements = []
for region in world.regions:
for location in region.locations:
if location.item and not location.event:
placements.append(location)
# fill in name of world for item
item_to_place.world = world
raise FillError(f'No more spots to place {item_to_place}, locations {locations} are invalid. '
@ -61,6 +60,7 @@ def fill_restrictive(world, base_state: CollectionState, locations, itempool, si
world.push_item(spot_to_fill, item_to_place, False)
locations.remove(spot_to_fill)
placements.append(spot_to_fill)
spot_to_fill.event = True
itempool.extend(unplaced_items)
@ -140,18 +140,30 @@ def distribute_items_restrictive(world, gftower_trash=False, fill_locations=None
if any(localprioitempool.values() or
localrestitempool.values()): # we need to make sure some fills are limited to certain worlds
local_locations = {player: [] for player in world.player_ids}
for location in fill_locations:
local_locations[location.player].append(location)
for locations in local_locations.values():
world.random.shuffle(locations)
for player, items in localprioitempool.items(): # items already shuffled
local_locations = [location for location in fill_locations if location.player == player]
world.random.shuffle(local_locations)
player_local_locations = local_locations[player]
for item_to_place in items:
spot_to_fill = local_locations.pop()
if not player_local_locations:
logging.warning(f"Ran out of local locations for player {player}, "
f"cannot place {item_to_place}.")
break
spot_to_fill = player_local_locations.pop()
world.push_item(spot_to_fill, item_to_place, False)
fill_locations.remove(spot_to_fill)
for player, items in localrestitempool.items(): # items already shuffled
local_locations = [location for location in fill_locations if location.player == player]
world.random.shuffle(local_locations)
player_local_locations = local_locations[player]
for item_to_place in items:
spot_to_fill = local_locations.pop()
if not player_local_locations:
logging.warning(f"Ran out of local locations for player {player}, "
f"cannot place {item_to_place}.")
break
spot_to_fill = player_local_locations.pop()
world.push_item(spot_to_fill, item_to_place, False)
fill_locations.remove(spot_to_fill)
@ -339,3 +351,69 @@ def balance_multiworld_progression(world):
break
elif not sphere_locations:
raise RuntimeError('Not all required items reachable. Something went terribly wrong here.')
def distribute_planned(world):
world_name_lookup = {world.player_names[player_id][0]: player_id for player_id in world.player_ids}
for player in world.player_ids:
placement: PlandoItem
for placement in world.plando_items[player]:
if placement.location in key_drop_data:
placement.warn(f"Can't place '{placement.item}' at '{placement.location}', as key drop shuffle locations are not supported yet.")
continue
item = ItemFactory(placement.item, player)
target_world: int = placement.world
if target_world is False or world.players == 1:
target_world = player # in own world
elif target_world is True: # in any other world
unfilled = list(location for location in world.get_unfilled_locations_for_players(
placement.location,
set(world.player_ids) - {player}) if location.item_rule(item)
)
if not unfilled:
placement.failed(f"Could not find a world with an unfilled location {placement.location}", FillError)
continue
target_world = world.random.choice(unfilled).player
elif target_world is None: # any random world
unfilled = list(location for location in world.get_unfilled_locations_for_players(
placement.location,
set(world.player_ids)) if location.item_rule(item)
)
if not unfilled:
placement.failed(f"Could not find a world with an unfilled location {placement.location}", FillError)
continue
target_world = world.random.choice(unfilled).player
elif type(target_world) == int: # target world by player id
if target_world not in range(1, world.players + 1):
placement.failed(f"Cannot place item in world {target_world} as it is not in range of (1, {world.players})", ValueError)
continue
else: # find world by name
if target_world not in world_name_lookup:
placement.failed(f"Cannot place item to {target_world}'s world as that world does not exist.", ValueError)
continue
target_world = world_name_lookup[target_world]
location = world.get_location(placement.location, target_world)
if location.item:
placement.failed(f"Cannot place item into already filled location {location}.")
continue
if location.can_fill(world.state, item, False):
world.push_item(location, item, collect=False)
location.event = True # flag location to be checked during fill
location.locked = True
logging.debug(f"Plando placed {item} at {location}")
else:
placement.failed(f"Can't place {item} at {location} due to fill condition not met.")
continue
if placement.from_pool: # Should happen AFTER the item is placed, in case it was allowed to skip failed placement.
try:
world.itempool.remove(item)
except ValueError:
placement.warn(f"Could not remove {item} from pool as it's already missing from it.")

View File

@ -401,6 +401,13 @@ async def on_client_joined(ctx: Context, client: Client):
ctx.notify_all(
f"{ctx.get_aliased_name(client.team, client.slot)} (Team #{client.team + 1}) has joined the game. "
f"Client({version_str}), {client.tags}).")
if client.version < [2, 1, 0] and "auto" in ctx.forfeit_mode:
ctx.notify_client(
client,
"Your client is too old to send game beaten information. "
"The automatic forfeit feature will not work."
)
ctx.client_connection_timers[client.team, client.slot] = datetime.datetime.now(datetime.timezone.utc)
async def on_client_left(ctx: Context, client: Client):
@ -467,8 +474,8 @@ def send_new_items(ctx: Context):
def forfeit_player(ctx: Context, team: int, slot: int):
all_locations = {values[0] for values in Regions.location_table.values() if type(values[0]) is int}
all_locations.update({values[1] for values in Regions.key_drop_data.values()})
# register any locations that are in the multidata
all_locations = {location_id for location_id, location_slot in ctx.locations if location_slot == slot}
ctx.notify_all("%s (Team #%d) has forfeited" % (ctx.player_names[(team, slot)], team + 1))
register_location_checks(ctx, team, slot, all_locations)
@ -954,20 +961,19 @@ class ClientMessageProcessor(CommonCommandProcessor):
self.output(response)
return False
def get_missing_checks(ctx: Context, client: Client) -> list:
locations = []
#for location_id in [k[0] for k, v in ctx.locations if k[1] == client.slot]:
# if location_id not in ctx.location_checks[client.team, client.slot]:
# locations.append(Regions.lookup_id_to_name.get(location_id, f'Unknown Location ID: {location_id}'))
for location_id, location_name in Regions.lookup_id_to_name.items(): # cheat console is -1, keep in mind
if location_id != -1 and location_id not in ctx.location_checks[client.team, client.slot] and (location_id, client.slot) in ctx.locations:
locations.append(location_name)
return locations
return [Regions.lookup_id_to_name.get(location_id, f'Unknown Location ID: {location_id}') for
location_id, slot in ctx.locations if
slot == client.slot and
location_id not in ctx.location_checks[client.team, client.slot]]
def get_client_points(ctx: Context, client: Client) -> int:
return (ctx.location_check_points * len(ctx.location_checks[client.team, client.slot]) -
ctx.hint_cost * ctx.hints_used[client.team, client.slot])
async def process_client_cmd(ctx: Context, client: Client, cmd, args):
if type(cmd) is not str:
await ctx.send_msgs(client, [['InvalidCmd']])

View File

@ -274,8 +274,11 @@ available_boss_names: typing.Set[str] = {boss.lower() for boss in Bosses.boss_ta
boss_shuffle_options = {None: 'none',
'none': 'none',
'simple': 'basic',
'basic': 'basic',
'full': 'normal',
'normal': 'normal',
'random': 'chaos',
'chaos': 'chaos',
'singularity': 'singularity',
'duality': 'singularity'
}
@ -368,12 +371,13 @@ def roll_settings(weights, plando_options: typing.Set[str] = frozenset(("bosses"
'triforce-hunt': 'triforcehunt', # deprecated, moving all goals to `_`
'local_triforce_hunt': 'localtriforcehunt',
'ganon_triforce_hunt': 'ganontriforcehunt',
'local_ganon_triforce_hunt': 'localganontriforcehunt'
'local_ganon_triforce_hunt': 'localganontriforcehunt',
'ice_rod_hunt': 'icerodhunt'
}[goal]
# TODO consider moving open_pyramid to an automatic variable in the core roller, set to True when
# fast ganon + ganon at hole
ret.open_pyramid = goal in {'fast_ganon', 'ganon_triforce_hunt', 'local_ganon_triforce_hunt', 'ganon_pedestal'}
ret.open_pyramid = ret.goal in {'crystals', 'ganontriforcehunt', 'localganontriforcehunt', 'ganonpedestal'}
ret.crystals_gt = prefer_int(get_choice('tower_open', weights))
@ -433,7 +437,7 @@ def roll_settings(weights, plando_options: typing.Set[str] = frozenset(("bosses"
bosses = []
for boss in options:
if boss in boss_shuffle_options:
remainder_shuffle = boss
remainder_shuffle = boss_shuffle_options[boss]
elif boss not in available_boss_names and not "-" in boss:
raise ValueError(f"Unknown Boss name or Boss shuffle option {boss}.")
else:
@ -555,21 +559,50 @@ def roll_settings(weights, plando_options: typing.Set[str] = frozenset(("bosses"
ret.plando_items = []
if "items" in plando_options:
default_placement = PlandoItem(item="", location="")
def add_plando_item(item: str, location: str):
if item not in item_table:
raise Exception(f"Could not plando item {item} as the item was not recognized")
if location not in location_table and location not in key_drop_data:
raise Exception(f"Could not plando item {item} at location {location} as the location was not recognized")
ret.plando_items.append(PlandoItem(item, location, location_world, from_pool, force))
options = weights.get("plando_items", [])
for placement in options:
if roll_percentage(get_choice("percentage", placement, 100)):
item = get_choice("item", placement)
location = get_choice("location", placement)
from_pool = get_choice("from_pool", placement, True)
location_world = get_choice("world", placement, False)
ret.plando_items.append(PlandoItem(item, location, location_world, from_pool))
from_pool = get_choice("from_pool", placement, default_placement.from_pool)
location_world = get_choice("world", placement, default_placement.world)
force = get_choice("force", placement, default_placement.force)
if "items" in placement and "locations" in placement:
items = placement["items"]
locations = placement["locations"]
if isinstance(items, dict):
item_list = []
for key, value in items.items():
item_list += [key] * value
items = item_list
if not items or not locations:
raise Exception("You must specify at least one item and one location to place items.")
random.shuffle(items)
random.shuffle(locations)
for item, location in zip(items, locations):
add_plando_item(item, location)
else:
item = get_choice("item", placement, get_choice("items", placement))
location = get_choice("location", placement)
add_plando_item(item, location)
ret.plando_texts = {}
if "texts" in plando_options:
tt = TextTable()
tt.removeUnwantedText()
options = weights.get("plando_texts", [])
for placement in options:
if roll_percentage(get_choice("percentage", placement, 100)):
ret.plando_texts[str(get_choice("at", placement))] = str(get_choice("text", placement))
at = str(get_choice("at", placement))
if at not in tt:
raise Exception(f"No text target \"{at}\" found.")
ret.plando_texts[at] = str(get_choice("text", placement))
ret.plando_connections = []
if "connections" in plando_options:

View File

@ -77,10 +77,14 @@ def register_session():
session["_id"] = uuid4() # uniquely identify each session without needing a login
@app.route('/tutorial/<string:game>/<string:file>/<string:lang>')
def tutorial(game, file, lang):
return render_template("tutorial.html", game=game, file=file, lang=lang)
@app.route('/tutorial')
@app.route('/tutorial/<string:lang>')
def tutorial(lang='en'):
return render_template(f"tutorial.html", lang=lang)
def tutorial_landing():
return render_template("tutorialLanding.html")
@app.route('/player-settings')

View File

@ -29,8 +29,9 @@ def download_spoiler(seed_id):
@app.route("/dl_raw_patch/<suuid:seed_id>/<int:player_id>")
def download_raw_patch(seed_id, player_id):
patch = select(patch for patch in Patch if patch.player_id == player_id and patch.seed.id == seed_id).first()
def download_raw_patch(seed_id, player_id: int):
patch = select(patch for patch in Patch if
patch.player_id == player_id and patch.seed.id == seed_id).first()
if not patch:
return "Patch not found"

View File

@ -1,10 +1,3 @@
const availableLanguages = {
de: 'Deutsch',
en: 'English',
es: 'Español',
fr: 'Français',
};
window.addEventListener('load', () => {
const tutorialWrapper = document.getElementById('tutorial-wrapper');
new Promise((resolve, reject) => {
@ -21,27 +14,11 @@ window.addEventListener('load', () => {
}
resolve(ajax.responseText);
};
ajax.open('GET', `${window.location.origin}/static/assets/tutorial/tutorial_` +
`${tutorialWrapper.getAttribute('data-language')}.md`, true);
ajax.open('GET', `${window.location.origin}/static/assets/tutorial/` +
`${tutorialWrapper.getAttribute('data-game')}/${tutorialWrapper.getAttribute('data-file')}_` +
`${tutorialWrapper.getAttribute('data-lang')}.md`, true);
ajax.send();
}).then((results) => {
// Build the language selector
let currentLanguage = window.location.href.split('/').pop();
if (Object.keys(availableLanguages).indexOf(currentLanguage) === -1) { currentLanguage = 'en' }
const languageSelectorWrapper = document.createElement('div');
languageSelectorWrapper.setAttribute('id', 'language-selector-wrapper')
const languageSelector = document.createElement('select');
languageSelector.setAttribute('id', 'language-selector');
for (const lang of Object.keys(availableLanguages)) {
const option = document.createElement('option');
option.value = lang;
option.innerText = availableLanguages[lang];
if (lang === currentLanguage) { option.setAttribute('selected', '1'); }
languageSelector.appendChild(option);
}
languageSelectorWrapper.appendChild(languageSelector);
tutorialWrapper.appendChild(languageSelectorWrapper);
// Populate page with HTML generated from markdown
tutorialWrapper.innerHTML += (new showdown.Converter()).makeHtml(results);
adjustHeaderWidth();
@ -65,14 +42,10 @@ window.addEventListener('load', () => {
console.error(error);
}
}
document.getElementById('language-selector').addEventListener('change', (event) => {
console.info(window.location.hostname);
window.location.href = `http://${window.location.hostname}/tutorial/${event.target.value}`;
});
}).catch((error) => {
console.error(error);
tutorialWrapper.innerHTML =
`<h2>${error}</h2>
`<h2>This page is out of logic!</h2>
<h3>Click <a href="${window.location.origin}/tutorial">here</a> to return to safety.</h3>`;
});
});

View File

@ -0,0 +1,75 @@
[
{
"gameTitle": "The Legend of Zelda: A Link to the Past",
"tutorials": [
{
"name": "Multiworld Setup Tutorial",
"description": "A guide to setting up the Archipelago ALttP software on your computer. This guide covers single-player, multiworld, and related software.",
"files": [
{
"language": "English",
"filename": "zelda3/multiworld_en.md",
"link": "zelda3/multiworld/en",
"authors": [
"Farrak Kilhn"
]
},
{
"language": "Deutsch",
"filename": "zelda3/multiworld_de.md",
"link": "zelda3/multiworld/de",
"authors": [
"Fischfilet"
]
},
{
"language": "Español",
"filename": "zelda3/multiworld_es.md",
"link": "zelda3/multiworld/es",
"authors": [
"Edos"
]
},
{
"language": "Français",
"filename": "zelda3/multiworld_fr.md",
"link": "zelda3/multiworld/fr",
"authors": [
"Coxla"
]
}
]
},
{
"name": "MSU-1 Setup Tutorial",
"description": "A guide to setting up MSU-1, which allows for custom in-game music.",
"files": [
{
"language": "English",
"filename": "zelda3/msu1_en.md",
"link": "zelda3/msu1/en",
"authors": [
"Farrak Kilhn"
]
},
{
"language": "Español",
"filename": "zelda3/msu1_es.md",
"link": "zelda3/msu1/es",
"authors": [
"Edos"
]
},
{
"language": "Français",
"filename": "msu1_fr.md",
"link": "zelda3/msu1/fr",
"authors": [
"Coxla"
]
}
]
}
]
}
]

View File

@ -0,0 +1,76 @@
# MSU-1 Setup Guide
## What is MSU-1?
MSU-1 allows for the use of custom in-game music. It works on original hardware, the SuperNT, and certain emulators.
This guide will explain how to find custom music packages, often called MSU packs, and how to configure
them for use with original hardware, the SuperNT, and the snes9x emulator.
## Where to find MSU Packs
MSU packs are constantly in development. You can find a list of completed packs, as well as in-development packs on
[this Google Spreadsheet](https://docs.google.com/spreadsheets/d/1XRkR4Xy6S24UzYkYBAOv-VYWPKZIoUKgX04RbjF128Q).
## What an MSU pack should look like
MSU packs contain many files, most of which are the music files which will be used when playing the game. These files
should be named similarly, with a hyphenated number at the end, and with a `.pcm` extension. It does not matter what
each music file is named, so long as they all follow the same pattern. The most popular filename you will find is
`alttp_msu-X.pcm`, where X is replaced by a number.
There is one other type of file you should find inside an MSU pack's folder. This file indicates to the hardware or
to the emulator that MSU should be enabled for this game. This file should be named similarly to the other files in
the folder, but will have a `.msu` extension and be 0 KB in size.
A short example of the contents of an MSU pack folder are as follows:
```
List of files inside an MSU pack folder:
alttp_msu.msu
alttp_msu-1.pcm
alttp_msu-2.pcm
...
alttp_msu-34.pcm
```
## How to use an MSU Pack
In all cases, you must rename your ROM file to match the pattern of names inside your MSU pack's folder, then place
your ROM file inside that folder.
This will cause the folder contents to look like the following:
```
List of files inside an MSU pack folder:
alttp_msu.msu
alttp_msu.sfc <-- Add your ROM file
alttp_msu-1.pcm
alttp_msu-2.pcm
...
alttp_msu-34.pcm
```
### With snes9x
1. Load the ROM file from snes9x.
### With SD2SNES / FXPak on original hardware
1. Load the MSU pack folder onto your SD2SNES / FXPak.
2. Navigate into the MSU pack folder and load your ROM.
### With SD2SNES / FXPak on SuperNT
1. Load the MSU pack folder onto your SD2SNES / FXPak.
2. Power on your SuperNT and navigate to the `Settings` menu.
3. Enter the `Audio` settings.
4. Check the box marked `Cartridge Audio Enable.`
5. Navigate back to the previous menu.
6. Choose `Save/Clear Settings`.
7. Choose `Save Settings`.
8. Choose `Run Cartridge` from the main menu.
9. Navigate into your MSU pack folder and load your ROM.
## A word of caution to streamers
Many MSU packs use copyrighted music which is not permitted for use on platforms like Twitch and YouTube.
If you choose to stream music from an MSU pack, please ensure you have permission to do so. If you stream
music which has not been licensed to you, or licensed for use in a stream in general, your VOD may be muted.
In the worst case, you may receive a DMCA take-down notice. Please be careful to only stream music for which
you have the rights to do so.
##### Stream-safe MSU packs
Below is a list of MSU packs which, so far as we know, are safe to stream. More will be added to this list as
we learn of them. If you know of any we missed, please let us know!
- Vanilla Game Music
- [Smooth McGroove](https://drive.google.com/open?id=1JDa1jCKg5hG0Km6xNpmIgf4kDMOxVp3n)

View File

@ -0,0 +1,74 @@
# MSU-1 Guía de instalación
## Que es MSU-1?
MSU-1 permite el uso de música personalizada durante el juego. Funciona en hardware original, la SuperNT, y algunos emuladores.
Esta guiá explicará como encontrar los packs de música personalizada, comúnmente llamados pack MSU, y como configurarlos
para su uso en hardware original, la SuperNT, and el emulador snes9x.
## Donde encontrar packs MSU
Los packs MSU están constantemente en desarrollo. Puedes encontrar una lista de pack completos, al igual que packs en desarrollo en
[esta hoja de calculo Google](https://docs.google.com/spreadsheets/d/1XRkR4Xy6S24UzYkYBAOv-VYWPKZIoUKgX04RbjF128Q).
## Que pinta debe tener un pack MSU
Los packs MSU contienen muchos ficheros, la mayoria de los cuales son los archivos de música que se usaran durante el juego. Estos ficheros
deben tener un nombre similar, con un guión seguido por un número al final, y tienen extensión`.pcm`. No importa como se llame
cada archivo de música, siempre y cuando todos sigan el mismo patrón. El nombre más popular es
`alttp_msu-X.pcm`, donde X es un número.
Hay otro tipo de fichero que deberias encontrar en el directorio de un pack MSU. Este archivo indica al hardware o
emulador que MSU debe ser activado para este juego. El fichero tiene un nombre similar al resto, pero tiene como extensión `.msu` y su tamaño es 0 KB.
Un pequeño ejemplo de los contenidos de un directorio que contiene un pack MSU:
```
Lista de ficheros dentro de un directorio de pack MSU:
alttp_msu.msu
alttp_msu-1.pcm
alttp_msu-2.pcm
...
alttp_msu-34.pcm
```
## Como usar un pack MSU
En todos los casos, debes renombrar tu fichero de ROM para que coincida con el resto de nombres de fichero del directorio, y copiar/pegar tu fichero rom
dentro de dicho directorio.
Esto hara que los contenidos del directorio sean los siguientes:
```
Lista de ficheros dentro del directorio de pack MSU:
alttp_msu.msu
alttp_msu.sfc <-- Tu fichero rom añadido
alttp_msu-1.pcm
alttp_msu-2.pcm
...
alttp_msu-34.pcm
```
### Con snes9x
1. Carga el fichero de rom en snes9x.
### Con SD2SNES / FXPak en hardware original
1. Carga tu directorio de pack MSU en tu SD2SNES / FXPak.
2. Navega hasta el directorio de pack MSU y carga la ROM
### Con SD2SNES / FXPak en SuperNT
1. Carga tu directorio de pack MSU en tu SD2SNES / FXPak.
2. Enciende tu SuperNT y navega al menú `Settings`.
3. Entra en la opcion `Audio`.
4. Activa la caja `Cartridge Audio Enable.`
5. Navega al menú anterior
6. Elije `Save/Clear Settings`.
7. Elije `Save Settings`.
8. Elije `Run Cartridge` en el menú principal.
9. Navega hasta el directorio de pack MSU y carga la ROM
## Aviso a streamers
Muchos packs MSU usan música con derechos de autor la cual no esta permitido su uso en plataformas como Twitch o YouTube.
Si elijes hacer stream de dicha música, tu VOD puede ser silenciado. En el peor caso, puedes recibir una orden de eliminación DMCA.
Por favor, tened cuidado y solo streamear música para la cual tengas los derechos para hacerlo.
##### Packs MSU seguros para Stream
A continuación enumeramos los packs MSU que, packs which, por lo que sabemos, son seguros para vuestras retransmisiones. Se iran añadiendo mas conforme
vayamos enterandonos. Si sabes alguno que podamos haber olvidado, por favor haznoslo saber!
- Musica del juego original
- [Smooth McGroove](https://drive.google.com/open?id=1JDa1jCKg5hG0Km6xNpmIgf4kDMOxVp3n)

View File

@ -0,0 +1,68 @@
# Guide d'installation de MSU-1
## Qu'est-ce que MSU-1 ?
MSU-1 permet l'utilisation de musiques en jeu personnalisées. Cela fonctionne sur une console originale, sur SuperNT, et sur certains émulateurs.
Ce guide explique comment trouver des packs de musiques personnalisées, couremment appelées packs MSU, et comment les configurer
pour les utiliser sur console, sur SuperNT et sur l'émulateur snes9x.
## Où trouver des packs MSU
Les packs MSU sont constamment en développement. Vous pouvez trouver une liste de packs complétés, ainsi que des packs en développement sur
[cette feuille de calcul Google](https://docs.google.com/spreadsheets/d/1XRkR4Xy6S24UzYkYBAOv-VYWPKZIoUKgX04RbjF128Q).
## A quoi ressemble un pack MSU
Les packs MSU contiennent beaucoup de fichiers, la plupart étant des fichiers musicaux qui seront utilisés en cours de jeu. Ces fichiers
doivent être nommés de façon similaire, avec un nombre derrière le tiret, puis l'extension `.pcm`. Le nom de chaque fichier
n'importe pas, du moment qu'ils suivent tous le même motif. Le nom le plus populaire que vous verrez est
`alttp_msu-X.pcm`, où X est remplacé par un nombre.
Il existe un autre type de fichier que vous devriez trouver dans le dossier d'un pack MSU. Ce fichier indique au matériel
ou à l'émulateur que MSU doit être activé pour ce jeu. Ce fichier doit être nommé de façon similaires aux autres dans
le dossier, mais il aura une extension `.msu` et pèsera 0 KB.
Voici un exemple de ce à quoi ressemble le dossier d'un pack MSU :
```
Liste des fichiers dans le dossier d'un pack MSU :
alttp_msu.msu
alttp_msu-1.pcm
alttp_msu-2.pcm
...
alttp_msu-34.pcm
```
## Comment utiliser un pack MSU
Dans tous les cas, vosu devez renommer votre fichier ROM pour qu'il corresponde au même motif que les autres fichiers dans le dossier du pack MSU,
ensuite vous placez votre fichier ROM dans ce dossier.
Le contenu du dossier ressemblera alors à ceci :
```
Liste des fichiers dans le dossier d'un pack MSU :
alttp_msu.msu
alttp_msu.sfc <-- Ajoutez votre fichier ROM
alttp_msu-1.pcm
alttp_msu-2.pcm
...
alttp_msu-34.pcm
```
### Avec snes9x
1. Chargez le fichier ROM depuis snes9x.
### Avec un SD2SNES / FXPak sur une console originale
1. Mettez le dossier du pack MSU avec la ROM sur votre SD2SNES / FXPak.
2. Naviguez vers ce dossier et chargez votre ROM.
### Avec un SD2SNES / FXPak sur SuperNT
1. Mettez le dossier du pack MSU avec la ROM sur votre SD2SNES / FXPak.
2. Allumez votre SuperNT et naviguez vers le menu `Settings` (paramètres).
3. Entrez dans les paramètres `Audio`.
4. Cochez la case marquée `Cartridge Audio Enable` (activer l'audio de cartouche).
5. Retournez dans le menu précédent.
6. Choisissez `Save/Clear Settings` (sauvegarder/effacer les paramètres).
7. Choisissez `Save Settings` (sauvegarder les paramètres).
8. Choisissez `Run Cartridge` (lancer une cartouche) depuis le menu principal.
9. Naviguez vers le dossier du pack MSU et chargez votre ROM.
## Avertissement pour les streamers
Beaucoup de packs MSU utilisent des musiques copyrightées ce qui n'est pas permis sur des plateformes comme Twitch et YouTube.
Si vous choisissez de streamer des musiques copyrightées, votre VOD sera peut-être rendue muette. Dans le pire des cas, vous pourriez recevoir
une plainte DMCA pour faire retirer la vidéo. Faites attention à streamer uniquement des musiques pour lesquelles vous avez le droit.

View File

@ -0,0 +1,77 @@
# A Link to the Past Randomizer Plando Guide
## Configuration
1. Plando features have to be enabled first, before they can be used (opt-in).
2. To do so, go to your installation directory (Windows default: C:\ProgramData\BerserkerMultiWorld),
then open the host.yaml file therein with a text editor.
3. In it, you're looking for the option key "plando_options",
to enable all plando modules you can set the value to "bosses, items, texts, connections"
## Modules
### Bosses
- This module is enabled by default and available to be used on
[https://archipelago.gg/generate](https://archipelago.gg/generate)
- Plando versions of boss shuffles can be added like any other boss shuffle option in a yaml and weighted.
- Boss Plando works as a list of instructions from left to right, if any arenas are empty at the end,
it defaults to vanilla
- Instructions are separated by a semicolon
- Available Instructions:
- Direct Placement:
- Example: "Eastern Palace-Trinexx"
- Takes a particular Arena and particular boss, then places that boss into that arena
- Ganons Tower has 3 placements, "Ganons Tower Top", "Ganons Tower Middle" and "Ganons Tower Bottom"
- Boss Placement:
- Example: "Trinexx"
- Takes a particular boss and places that boss in any remaining slots in which this boss can function.
- In this example, it would fill Desert Palace, but not Tower of Hera.
- Boss Shuffle:
- Example: "simple"
- Runs a particular boss shuffle mode to finish construction instead of vanilla placement, typically used as a last instruction.
- [Available Bosses](https://github.com/Berserker66/MultiWorld-Utilities/blob/65fa39df95c90c9b66141aee8b16b7e560d00819/Bosses.py#L135)
- [Available Arenas](https://github.com/Berserker66/MultiWorld-Utilities/blob/65fa39df95c90c9b66141aee8b16b7e560d00819/Bosses.py#L186)
#### Examples:
```yaml
boss_shuffle:
Turtle Rock-Trinexx;basic: 1
full: 2
Mothula: 3
Ganons Tower Bottom-Kholdstare;Trinexx;Kholdstare: 4
```
1. Would be basic boss shuffle but prevent Trinexx from appearing outside of Turtle Rock,
as there's only one Trinexx in the pool
2. Regular full boss shuffle. With a 2 in 10 chance to occur.
3. A Mothula Singularity, as Mothula works in any arena.
4. A Trinexx -> Kholdstare Singularity that prevents ice Trinexx in GT
### Text
- This module is disabled by default.
- Has the options "text", "at" and "percentage"
- percentage is the percentage chance for this text to be placed, can be omitted entirely for 100%
- text is the text to be placed.
- can be weighted.
- \n is a newline.
- @ is the entered player's name.
- Warning: Text Mapper does not support full unicode.
- [Alphabet](https://github.com/Berserker66/MultiWorld-Utilities/blob/65fa39df95c90c9b66141aee8b16b7e560d00819/Text.py#L756)
- at is the location within the game to attach the text to.
- can be weighted.
- [List of targets](https://github.com/Berserker66/MultiWorld-Utilities/blob/65fa39df95c90c9b66141aee8b16b7e560d00819/Text.py#L1498)
#### Example
```yaml
plando_texts:
- text: "This is a plando.\nYou've been warned."
at:
uncle_leaving_text: 1
uncle_dying_sewer: 1
percentage: 50
```
![Uncle Example](https://cdn.discordapp.com/attachments/731214280439103580/794953870903083058/unknown.png)
This has a 50% chance to trigger at all, if it does,
it throws a coin between "uncle_leaving_text" and "uncle_dying_sewer", then places the text
"This is a plando.\nYou've been warned." at that location.

View File

@ -0,0 +1,71 @@
const showError = () => {
const tutorial = document.getElementById('tutorial-landing');
document.getElementById('page-title').innerText = 'This page is out of logic!';
tutorial.removeChild(document.getElementById('loading'));
const userMessage = document.createElement('h3');
const homepageLink = document.createElement('a');
homepageLink.innerText = 'Click here';
homepageLink.setAttribute('href', '/');
userMessage.append(homepageLink);
userMessage.append(' to go back to safety!');
tutorial.append(userMessage);
};
window.addEventListener('load', () => {
const ajax = new XMLHttpRequest();
ajax.onreadystatechange = () => {
if (ajax.readyState !== 4) { return; }
const tutorialDiv = document.getElementById('tutorial-landing');
if (ajax.status !== 200) { return showError(); }
try {
const games = JSON.parse(ajax.responseText);
games.forEach((game) => {
const gameTitle = document.createElement('h2');
gameTitle.innerText = game.gameTitle;
tutorialDiv.appendChild(gameTitle);
game.tutorials.forEach((tutorial) => {
const tutorialName = document.createElement('h3');
tutorialName.innerText = tutorial.name;
tutorialDiv.appendChild(tutorialName);
const tutorialDescription = document.createElement('p');
tutorialDescription.innerText = tutorial.description;
tutorialDiv.appendChild(tutorialDescription);
const intro = document.createElement('p');
intro.innerText = 'This guide is available in the following languages:';
tutorialDiv.appendChild(intro);
const fileList = document.createElement('ul');
tutorial.files.forEach((file) => {
const listItem = document.createElement('li');
const anchor = document.createElement('a');
anchor.innerText = file.language;
anchor.setAttribute('href', `${window.location.origin}/tutorial/${file.link}`);
listItem.appendChild(anchor);
listItem.append(' by ');
for (let author of file.authors) {
listItem.append(author);
if (file.authors.indexOf(author) !== (file.authors.length -1)) {
listItem.append(', ');
}
}
fileList.appendChild(listItem);
});
tutorialDiv.appendChild(fileList);
});
});
tutorialDiv.removeChild(document.getElementById('loading'));
} catch (error) {
showError();
console.error(error);
}
};
ajax.open('GET', `${window.location.origin}/static/assets/tutorial/tutorials.json`, true);
ajax.send();
});

View File

@ -1017,7 +1017,7 @@
"expert": {
"keyString": "enemy_health.expert",
"friendlyName": "Armor-Plated",
"description": "Enemies will be very heard to defeat.",
"description": "Enemies will be very hard to defeat.",
"defaultValue": 0
}
}

View File

@ -16,6 +16,10 @@ html{
color: #eeffeb;
}
#tutorial-wrapper p{
margin-top: 0;
}
#tutorial-wrapper a{
color: #ffef00;
}
@ -90,6 +94,7 @@ html{
}
#tutorial-wrapper pre{
margin-top: 0;
padding: 0.5rem 0.25rem;
background-color: #ffeeab;
border: 1px solid #9f916a;

View File

@ -0,0 +1,106 @@
html{
background-image: url('../static/backgrounds/grass/grass-0007-large.png');
background-repeat: repeat;
background-size: 650px 650px;
}
#tutorial-landing{
display: flex;
flex-direction: column;
max-width: 70rem;
margin-left: auto;
margin-right: auto;
background-color: rgba(0, 0, 0, 0.15);
border-radius: 8px;
padding: 1rem 1rem 3rem;
color: #eeffeb;
}
#tutorial-landing p{
margin: 0;
}
#tutorial-landing a{
color: #ffef00;
text-decoration: none;
}
#tutorial-landing h1{
font-size: 2.5rem;
font-weight: normal;
border-bottom: 1px solid #ffffff;
width: 100%;
margin-bottom: 0.5rem;
color: #ffffff;
text-shadow: 1px 1px 4px #000000;
}
#tutorial-landing h2{
font-size: 2rem;
font-weight: normal;
border-bottom: 1px solid #ffffff;
width: 100%;
margin-bottom: 0.5rem;
color: #ffe993;
text-transform: lowercase;
text-shadow: 1px 1px 2px #000000;
}
#tutorial-landing h3{
font-size: 1.70rem;
font-weight: normal;
text-align: left;
width: 100%;
margin-bottom: 0.5rem;
}
#tutorial-landing h4{
font-size: 1.5rem;
font-weight: normal;
margin-bottom: 0.5rem;
}
#tutorial-landing h5{
font-size: 1.25rem;
font-weight: normal;
}
#tutorial-landing h6{
font-size: 1.25rem;
font-weight: normal;
color: #434343;
}
#tutorial-landing h3, #tutorial-landing h4, #tutorial-landing h5,#tutorial-landing h6{
color: #ffffff;
text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.5);
}
#tutorial-landing ul{
}
#tutorial-landing ol{
}
#tutorial-landing li{
}
#tutorial-landing pre{
margin-top: 0;
padding: 0.5rem 0.25rem;
background-color: #ffeeab;
border: 1px solid #9f916a;
border-radius: 6px;
color: #000000;
}
#tutorial-landing code{
background-color: #ffeeab;
border-radius: 4px;
padding-left: 0.25rem;
padding-right: 0.25rem;
color: #000000;
}

View File

@ -8,12 +8,12 @@
<a href="/" id="site-title">
<img src="/favicon.ico" alt="Favicon" />
</a>
<a href="/">multiworld randomizer</a>
<a href="/">archipelago</a>
</div>
<div id="base-header-right">
<a href="/player-settings">start game</a>
<a href="/uploads">host game</a>
<a href="/tutorial">setup guide</a>
<a href="/tutorial">setup guides</a>
<a href="/generate">upload config</a>
<a href="https://discord.gg/8Z65BR2">discord</a>
</div>

View File

@ -14,8 +14,12 @@
<div id="host-game" class="grass-island {% if rooms %}wider{% endif %}">
<h1>Host Game</h1>
<p>
To host a game, you need to upload a .multidata file or a .zip file<br />
created by the multiworld generator.
This page allows you to host a game which was not generated by the website. For example, if you have
generated a doors game on your own computer, you may upload the zip file created by the generator to
host the game here. This will also provide the tracker, and the ability for your players to download
their patch files.
<br /><br />
In addition to a zip file created by the generator, you may upload a multidata file here as well.
</p>
<div id="host-game-form-wrapper">
<form id="host-game-form" method="post" enctype="multipart/form-data">

View File

@ -20,9 +20,9 @@
later,
you can simply refresh this page and the server will be started again.<br>
{% if room.last_port %}
You can connect to this room by using '/connect berserkermulti.world:{{ room.last_port }}'
You can connect to this room by using '/connect archipelago.gg:{{ room.last_port }}'
in the <a href="https://github.com/Berserker66/MultiWorld-Utilities/releases">client</a>.<br>{% endif %}
{{ macros.list_patches_room(room.seed.patches, room) }}
{{ macros.list_patches_room(room) }}
{% if room.owner == session["_id"] %}
<form method=post>
<div class="form-group">

View File

@ -14,7 +14,7 @@
<div id="landing-links">
<a href="/player-settings" id="player-settings-button">start<br />playing</a>
<a href="/uploads" id="uploads-button">host<br />game</a>
<a href="/tutorial" id="setup-guide-button">setup guide</a>
<a href="/tutorial" id="setup-guide-button">setup guides</a>
<a href="/generate" id="generate-button">upload config</a>
<a href="https://discord.gg/8Z65BR2" id="discord-button">discord</a>
</div>

View File

@ -6,8 +6,8 @@
{{ caller() }}
</ul>
{%- endmacro %}
{% macro list_patches_room(patches, room) %}
{% if patches %}
{% macro list_patches_room(room) %}
{% if room.seed.patches %}
<ul>
{% for patch in patches|list|sort(attribute="player") %}
<li><a href="{{ url_for("download_patch", patch_id=patch.id, room_id=room.id) }}">

View File

@ -2,7 +2,7 @@
{% block head %}
{% include 'header/grassHeader.html' %}
<title>Setup Tutorial</title>
<title>Archipelago</title>
<link rel="stylesheet" type="text/css" href="{{ static_autoversion("styles/tutorial.css") }}" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/showdown/1.9.1/showdown.min.js"
integrity="sha512-L03kznCrNOfVxOUovR6ESfCz9Gfny7gihUX/huVbQB9zjODtYpxaVtIaAkpetoiyV2eqWbvxMH9fiSv5enX7bw=="
@ -11,7 +11,7 @@
{% endblock %}
{% block body %}
<div id="tutorial-wrapper" class="main-content" data-language="{{ lang }}">
<div id="tutorial-wrapper" data-game="{{ game }}" data-file="{{ file }}" data-lang="{{ lang }}">
<!-- Content generated by JavaScript -->
</div>
{% endblock %}

View File

@ -0,0 +1,15 @@
{% extends 'pageWrapper.html' %}
{% block head %}
{% include 'header/grassHeader.html' %}
<title>Archipelago Guides</title>
<link rel="stylesheet" type="text/css" href="{{ static_autoversion("styles/tutorialLanding.css") }}" />
<script type="application/ecmascript" src="{{ static_autoversion("assets/tutorialLanding.js") }}"></script>
{% endblock %}
{% block body %}
<div id="tutorial-landing" data-game="{{ game }}" data-file="{{ file }}" data-lang="{{ lang }}">
<h1 id="page-title">Archipelago Guides</h1>
<p id="loading">Loading...</p>
</div>
{% endblock %}

View File

@ -33,26 +33,27 @@
</tr>
{% endif %}
{% if seed.multidata %}
<tr>
<td>Players:&nbsp;</td>
<td>
<ul>
{% for team in seed.multidata["names"] %}
<li>Team #{{ loop.index }} - {{ team | length }}
<ul>
{% for player in team %}
<li>
<a href="{{ url_for("download_raw_patch", seed_id=seed.id, player_id=loop.index) }}">{{ player }}</a>
</li>
{% endfor %}
</ul>
</li>
{% endfor %}
</ul>
</td>
</tr>
<tr>
<td>Rooms:&nbsp;</td>
<tr>
<td>Players:&nbsp;</td>
<td>
<ul>
{% for team in seed.multidata["names"] %}
{% set outer_loop = loop %}
<li>Team #{{ loop.index }} - {{ team | length }}
<ul>
{% for player in team %}
<li>
<a href="{{ url_for("download_raw_patch", seed_id=seed.id, player_id=loop.index, team_id=outer_loop.index0) }}">{{ player }}</a>
</li>
{% endfor %}
</ul>
</li>
{% endfor %}
</ul>
</td>
</tr>
<tr>
<td>Rooms:&nbsp;</td>
<td>
{% call macros.list_rooms(rooms) %}
<li>
@ -69,7 +70,7 @@
{% for patch in seed.patches %}
<li>
<a href="{{ url_for("download_raw_patch", seed_id=seed.id, player_id=patch.player) }}">Player {{ patch.player }}</a>
<a href="{{ url_for("download_raw_patch", seed_id=seed.id, player_id=patch.player, team_id=0) }}">Player {{ patch.player }}</a>
</li>

View File

@ -39,7 +39,8 @@ def uploads():
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."
elif file.filename.endswith(".apbp"):
player = int(file.filename.split("P")[1].split(".")[0].split("_")[0])
splitted = file.filename.split("/")[-1][3:].split("P", 1)
player = int(splitted[1].split(".")[0].split("_")[0])
patches.add(Patch(data=zfile.open(file, "r").read(), player=player))
elif file.filename.endswith(".txt"):
spoiler = zfile.open(file, "r").read().decode("utf-8-sig")

View File

@ -135,10 +135,11 @@ def fill_dungeons_restrictive(world):
elif (item.map and world.mapshuffle[item.player]) or (item.compass and world.compassshuffle[item.player]):
item.priority = True
dungeon_items = [item for item in get_dungeon_item_pool(world) if ((item.smallkey and not world.keyshuffle[item.player])
dungeon_items = [item for item in get_dungeon_item_pool(world) if (((item.smallkey and not world.keyshuffle[item.player])
or (item.bigkey and not world.bigkeyshuffle[item.player])
or (item.map and not world.mapshuffle[item.player])
or (item.compass and not world.compassshuffle[item.player]))]
or (item.compass and not world.compassshuffle[item.player])
) and world.goal[item.player] != 'icerodhunt')] #
if dungeon_items:
# sort in the order Big Key, Small Key, Other before placing dungeon items
sort_order = {"BigKey": 3, "SmallKey": 2}

View File

@ -7,6 +7,7 @@ from worlds.alttp.Dungeons import get_dungeon_item_pool
from worlds.alttp.EntranceShuffle import connect_entrance
from Fill import FillError, fill_restrictive
from worlds.alttp.Items import ItemFactory
from Rules import forbid_items_for_player
# This file sets the item pools for various modes. Timed modes and triforce hunt are enforced first, and then extra items are specified per mode to fill in the remaining space.
# Some basic items that various modes require are placed here, including pendants and crystals. Medallion requirements for the two relevant entrances are also decided.
@ -17,6 +18,7 @@ alwaysitems = ['Bombos', 'Book of Mudora', 'Cane of Somaria', 'Ether', 'Fire Rod
'Cane of Byrna', 'Blue Boomerang', 'Red Boomerang']
progressivegloves = ['Progressive Glove'] * 2
basicgloves = ['Power Glove', 'Titans Mitts']
legacyinsanity = ['Magic Mirror', 'Moon Pearl']
normalbottles = ['Bottle', 'Bottle (Red Potion)', 'Bottle (Green Potion)', 'Bottle (Blue Potion)', 'Bottle (Fairy)',
'Bottle (Bee)', 'Bottle (Good Bee)']
@ -43,6 +45,7 @@ Difficulty = namedtuple('Difficulty',
['baseitems', 'bottles', 'bottle_count', 'same_bottle', 'progressiveshield',
'basicshield', 'progressivearmor', 'basicarmor', 'swordless', 'progressivemagic', 'basicmagic',
'progressivesword', 'basicsword', 'progressivebow', 'basicbow', 'timedohko', 'timedother',
'progressiveglove', 'basicglove', 'alwaysitems', 'legacyinsanity',
'universal_keys',
'extras', 'progressive_sword_limit', 'progressive_shield_limit',
'progressive_armor_limit', 'progressive_bottle_limit',
@ -69,6 +72,10 @@ difficulties = {
basicbow=['Bow', 'Silver Bow'] * 2,
timedohko=['Green Clock'] * 25,
timedother=['Green Clock'] * 20 + ['Blue Clock'] * 10 + ['Red Clock'] * 10,
progressiveglove=progressivegloves,
basicglove=basicgloves,
alwaysitems=alwaysitems,
legacyinsanity=legacyinsanity,
universal_keys=['Small Key (Universal)'] * 28,
extras=[easyfirst15extra, easysecond15extra, easythird10extra, easyfourth5extra, easyfinal25extra],
progressive_sword_limit=8,
@ -97,6 +104,10 @@ difficulties = {
basicbow=['Bow', 'Silver Bow'],
timedohko=['Green Clock'] * 25,
timedother=['Green Clock'] * 20 + ['Blue Clock'] * 10 + ['Red Clock'] * 10,
progressiveglove=progressivegloves,
basicglove=basicgloves,
alwaysitems=alwaysitems,
legacyinsanity=legacyinsanity,
universal_keys=['Small Key (Universal)'] * 18 + ['Rupees (20)'] * 10,
extras=[normalfirst15extra, normalsecond15extra, normalthird10extra, normalfourth5extra, normalfinal25extra],
progressive_sword_limit=4,
@ -125,6 +136,10 @@ difficulties = {
basicbow=['Bow'] * 2,
timedohko=['Green Clock'] * 25,
timedother=['Green Clock'] * 20 + ['Blue Clock'] * 10 + ['Red Clock'] * 10,
progressiveglove=progressivegloves,
basicglove=basicgloves,
alwaysitems=alwaysitems,
legacyinsanity=legacyinsanity,
universal_keys=['Small Key (Universal)'] * 12 + ['Rupees (5)'] * 16,
extras=[normalfirst15extra, normalsecond15extra, normalthird10extra, normalfourth5extra, normalfinal25extra],
progressive_sword_limit=3,
@ -154,6 +169,10 @@ difficulties = {
basicbow=['Bow'] * 2,
timedohko=['Green Clock'] * 20 + ['Red Clock'] * 5,
timedother=['Green Clock'] * 20 + ['Blue Clock'] * 10 + ['Red Clock'] * 10,
progressiveglove=progressivegloves,
basicglove=basicgloves,
alwaysitems=alwaysitems,
legacyinsanity=legacyinsanity,
universal_keys=['Small Key (Universal)'] * 12 + ['Rupees (5)'] * 16,
extras=[normalfirst15extra, normalsecond15extra, normalthird10extra, normalfourth5extra, normalfinal25extra],
progressive_sword_limit=2,
@ -166,11 +185,46 @@ difficulties = {
),
}
ice_rod_hunt_difficulties = dict()
for diff in {'easy', 'normal', 'hard', 'expert'}:
ice_rod_hunt_difficulties[diff] = Difficulty(
baseitems=['Nothing'] * 41,
bottles=['Nothing'] * 4,
bottle_count=difficulties[diff].bottle_count,
same_bottle=difficulties[diff].same_bottle,
progressiveshield=['Nothing'] * 3,
basicshield=['Nothing'] * 3,
progressivearmor=['Nothing'] * 2,
basicarmor=['Nothing'] * 2,
swordless=['Nothing'] * 4,
progressivemagic=['Nothing'] * 2,
basicmagic=['Nothing'] * 2,
progressivesword=['Nothing'] * 4,
basicsword=['Nothing'] * 4,
progressivebow=['Nothing'] * 2,
basicbow=['Nothing'] * 2,
timedohko=difficulties[diff].timedohko,
timedother=difficulties[diff].timedother,
progressiveglove=['Nothing'] * 2,
basicglove=['Nothing'] * 2,
alwaysitems=['Ice Rod'] + ['Nothing'] * 19,
legacyinsanity=['Nothing'] * 2,
universal_keys=['Nothing'] * 28,
extras=[['Nothing'] * 15, ['Nothing'] * 15, ['Nothing'] * 10, ['Nothing'] * 5, ['Nothing'] * 25],
progressive_sword_limit=difficulties[diff].progressive_sword_limit,
progressive_shield_limit=difficulties[diff].progressive_shield_limit,
progressive_armor_limit=difficulties[diff].progressive_armor_limit,
progressive_bow_limit=difficulties[diff].progressive_bow_limit,
progressive_bottle_limit=difficulties[diff].progressive_bottle_limit,
boss_heart_container_limit=difficulties[diff].boss_heart_container_limit,
heart_piece_limit=difficulties[diff].heart_piece_limit,
)
def generate_itempool(world, player: int):
if world.difficulty[player] not in difficulties:
raise NotImplementedError(f"Diffulty {world.difficulty[player]}")
if world.goal[player] not in {'ganon', 'pedestal', 'dungeons', 'triforcehunt', 'localtriforcehunt',
if world.goal[player] not in {'ganon', 'pedestal', 'dungeons', 'triforcehunt', 'localtriforcehunt', 'icerodhunt',
'ganontriforcehunt', 'localganontriforcehunt', 'crystals', 'ganonpedestal'}:
raise NotImplementedError(f"Goal {world.goal[player]}")
if world.mode[player] not in {'open', 'standard', 'inverted'}:
@ -180,7 +234,7 @@ def generate_itempool(world, player: int):
if world.timer[player] in ['ohko', 'timed-ohko']:
world.can_take_damage[player] = False
if world.goal[player] in ['pedestal', 'triforcehunt', 'localtriforcehunt']:
if world.goal[player] in ['pedestal', 'triforcehunt', 'localtriforcehunt', 'icerodhunt']:
world.push_item(world.get_location('Ganon', player), ItemFactory('Nothing', player), False)
else:
world.push_item(world.get_location('Ganon', player), ItemFactory('Triforce', player), False)
@ -200,6 +254,46 @@ def generate_itempool(world, player: int):
loc.event = True
loc.locked = True
if world.goal[player] == 'icerodhunt':
world.progression_balancing[player] = False
loc = world.get_location('Turtle Rock - Boss', player)
world.push_item(loc, ItemFactory('Triforce', player), False)
if world.boss_shuffle[player] != 'none':
if 'turtle rock-' not in world.boss_shuffle[player]:
world.boss_shuffle[player] = f'Turtle Rock-Trinexx;{world.boss_shuffle[player]}'
else:
logging.warning(f'Cannot guarantee that Trinexx is the boss of Turtle Rock for player {player}')
loc.event = True
loc.locked = True
forbid_items_for_player(loc, {'Red Pendant', 'Green Pendant', 'Blue Pendant', 'Crystal 5', 'Crystal 6'}, player)
itemdiff = difficulties[world.difficulty[player]]
itempool = []
itempool.extend(itemdiff.alwaysitems)
itempool.remove('Ice Rod')
itempool.extend(['Single Arrow', 'Sanctuary Heart Container'])
itempool.extend(['Boss Heart Container'] * itemdiff.boss_heart_container_limit)
itempool.extend(['Piece of Heart'] * itemdiff.heart_piece_limit)
itempool.extend(itemdiff.bottles)
itempool.extend(itemdiff.basicbow)
itempool.extend(itemdiff.basicarmor)
if world.swords[player] != 'swordless':
itempool.extend(itemdiff.basicsword)
itempool.extend(itemdiff.basicmagic)
itempool.extend(itemdiff.basicglove)
itempool.extend(itemdiff.basicshield)
itempool.extend(itemdiff.legacyinsanity)
itempool.extend(['Rupees (300)'] * 34)
itempool.extend(['Bombs (10)'] * 5)
itempool.extend(['Arrows (10)'] * 7)
if world.keyshuffle[player] == 'universal':
itempool.extend(itemdiff.universal_keys)
itempool.append('Small Key (Universal)')
for item in itempool:
world.push_precollected(ItemFactory(item, player))
world.get_location('Ganon', player).event = True
world.get_location('Ganon', player).locked = True
world.push_item(world.get_location('Agahnim 1', player), ItemFactory('Beat Agahnim 1', player), False)
@ -271,18 +365,26 @@ def generate_itempool(world, player: int):
if treasure_hunt_icon is not None:
world.treasure_hunt_icon[player] = treasure_hunt_icon
world.itempool.extend([item for item in get_dungeon_item_pool(world) if item.player == player
dungeon_items = [item for item in get_dungeon_item_pool(world) if item.player == player
and ((item.smallkey and world.keyshuffle[player])
or (item.bigkey and world.bigkeyshuffle[player])
or (item.map and world.mapshuffle[player])
or (item.compass and world.compassshuffle[player]))])
or (item.compass and world.compassshuffle[player])
or world.goal[player] == 'icerodhunt')]
if world.goal[player] == 'icerodhunt':
for item in dungeon_items:
world.itempool.append(ItemFactory('Nothing', player))
world.push_precollected(item)
else:
world.itempool.extend([item for item in dungeon_items])
# logic has some branches where having 4 hearts is one possible requirement (of several alternatives)
# rather than making all hearts/heart pieces progression items (which slows down generation considerably)
# We mark one random heart container as an advancement item (or 4 heart pieces in expert mode)
if world.difficulty[player] in ['easy', 'normal', 'hard'] and not (world.custom and world.customitemarray[30] == 0):
if world.goal[player] != 'icerodhunt' and world.difficulty[player] in ['easy', 'normal', 'hard'] and not (world.custom and world.customitemarray[30] == 0):
next(item for item in items if item.name == 'Boss Heart Container').advancement = True
elif world.difficulty[player] in ['expert'] and not (world.custom and world.customitemarray[29] < 4):
elif world.goal[player] != 'icerodhunt' and world.difficulty[player] in ['expert'] and not (world.custom and world.customitemarray[29] < 4):
adv_heart_pieces = (item for item in items if item.name == 'Piece of Heart')
for i in range(4):
next(adv_heart_pieces).advancement = True
@ -348,6 +450,7 @@ def shuffle_shops(world, items, player: int):
if 'u' in option:
progressive = world.progressive[player]
progressive = world.random.choice([True, False]) if progressive == 'random' else progressive == 'on'
progressive &= world.goal == 'icerodhunt'
new_items = ["Bomb Upgrade (+5)"] * 6
new_items.append("Bomb Upgrade (+5)" if progressive else "Bomb Upgrade (+10)")
@ -362,13 +465,17 @@ def shuffle_shops(world, items, player: int):
shop.region.name == "Capacity Upgrade":
shop.clear_inventory()
for i, item in enumerate(items):
if not "Heart" in item.name:
items[i] = ItemFactory(new_items.pop(), player)
if not new_items:
break
if world.goal[player] != 'icerodhunt':
for i, item in enumerate(items):
if "Heart" not in item.name:
items[i] = ItemFactory(new_items.pop(), player)
if not new_items:
break
else:
logging.warning(f"Not all upgrades put into Player{player}' item pool. Still missing: {new_items}")
else:
logging.warning(f"Not all upgrades put into Player{player}' item pool. Still missing: {new_items}")
for item in new_items:
world.push_precollected(ItemFactory(item, player))
if 'p' in option or 'i' in option:
shops = []
@ -417,10 +524,17 @@ take_any_locations = {
'Palace of Darkness Hint', 'East Dark World Hint', 'Archery Game', 'Dark Lake Hylia Ledge Hint',
'Dark Lake Hylia Ledge Spike Cave', 'Fortune Teller (Dark)', 'Dark Sanctuary Hint', 'Dark Desert Hint'}
take_any_locations_inverted = list(take_any_locations - {"Dark Sanctuary Hint", "Archery Game"})
take_any_locations = list(take_any_locations)
# sets are sorted by the element's hash, python's hash is seeded at startup, resulting in different sorting each run
take_any_locations_inverted.sort()
take_any_locations.sort()
def set_up_take_anys(world, player):
# these are references, do not modify these lists in-place
if world.mode[player] == 'inverted':
take_any_locs = take_any_locations - {"Dark Sanctuary Hint", "Archery Game"}
take_any_locs = take_any_locations_inverted
else:
take_any_locs = take_any_locations
@ -553,7 +667,8 @@ def get_pool_core(world, player: int):
treasure_hunt_count = None
treasure_hunt_icon = None
pool.extend(alwaysitems)
diff = ice_rod_hunt_difficulties[difficulty] if goal == 'icerodhunt' else difficulties[difficulty]
pool.extend(diff.alwaysitems)
def place_item(loc, item):
assert loc not in placed_items
@ -563,37 +678,35 @@ def get_pool_core(world, player: int):
return world.random.choice([True, False]) if progressive == 'random' else progressive == 'on'
# provide boots to major glitch dependent seeds
if logic in {'owglitches', 'nologic'} and world.glitch_boots[player]:
if logic in {'owglitches', 'nologic'} and world.glitch_boots[player] and goal != 'icerodhunt':
precollected_items.append('Pegasus Boots')
pool.remove('Pegasus Boots')
pool.append('Rupees (20)')
if want_progressives():
pool.extend(progressivegloves)
pool.extend(diff.progressiveglove)
else:
pool.extend(basicgloves)
pool.extend(diff.basicglove)
# insanity legacy shuffle doesn't have fake LW/DW logic so for now guaranteed Mirror and Moon Pearl at the start
if shuffle == 'insanity_legacy':
place_item('Link\'s House', 'Magic Mirror')
place_item('Sanctuary', 'Moon Pearl')
place_item('Link\'s House', diff.legacyinsanity[0])
place_item('Sanctuary', diff.legacyinsanity[1])
else:
pool.extend(['Magic Mirror', 'Moon Pearl'])
pool.extend(diff.legacyinsanity)
if timer == 'display':
clock_mode = 'stopwatch'
elif timer == 'ohko':
clock_mode = 'ohko'
diff = difficulties[difficulty]
pool.extend(diff.baseitems)
# expert+ difficulties produce the same contents for
# all bottles, since only one bottle is available
if diff.same_bottle:
thisbottle = world.random.choice(diff.bottles)
thisbottle = None
for _ in range(diff.bottle_count):
if not diff.same_bottle:
if not diff.same_bottle or not thisbottle:
thisbottle = world.random.choice(diff.bottles)
pool.append(thisbottle)
@ -614,7 +727,7 @@ def get_pool_core(world, player: int):
if want_progressives():
pool.extend(diff.progressivebow)
elif swords == 'swordless' or logic == 'noglitches':
elif (swords == 'swordless' or logic == 'noglitches') and goal != 'icerodhunt':
swordless_bows = ['Bow', 'Silver Bow']
if difficulty == "easy":
swordless_bows *= 2
@ -641,7 +754,7 @@ def get_pool_core(world, player: int):
else:
progressive_swords = want_progressives()
pool.extend(diff.progressivesword if progressive_swords else diff.basicsword)
if swords == 'assured':
if swords == 'assured' and goal != 'icerodhunt':
if progressive_swords:
precollected_items.append('Progressive Sword')
pool.remove('Progressive Sword')
@ -688,13 +801,14 @@ def get_pool_core(world, player: int):
pool = ['Rupees (5)' if item in replace else item for item in pool]
if world.keyshuffle[player] == "universal":
pool.extend(diff.universal_keys)
item_to_place = 'Small Key (Universal)' if goal != 'icerodhunt' else 'Nothing'
if mode == 'standard':
key_location = world.random.choice(
['Secret Passage', 'Hyrule Castle - Boomerang Chest', 'Hyrule Castle - Map Chest',
'Hyrule Castle - Zelda\'s Chest', 'Sewers - Dark Cross'])
place_item(key_location, 'Small Key (Universal)')
place_item(key_location, item_to_place)
else:
pool.extend(['Small Key (Universal)'])
pool.extend([item_to_place])
return (pool, placed_items, precollected_items, clock_mode, treasure_hunt_count, treasure_hunt_icon,
additional_pieces_to_place)
@ -798,10 +912,9 @@ def make_custom_item_pool(world, player):
# expert+ difficulties produce the same contents for
# all bottles, since only one bottle is available
if diff.same_bottle:
thisbottle = world.random.choice(diff.bottles)
thisbottle = None
for _ in range(customitemarray[18]):
if not diff.same_bottle:
if not diff.same_bottle or not thisbottle:
thisbottle = world.random.choice(diff.bottles)
pool.append(thisbottle)

View File

@ -18,7 +18,7 @@ from worlds.alttp.EntranceShuffle import link_entrances, link_inverted_entrances
from worlds.alttp.Rom import patch_rom, patch_race_rom, patch_enemizer, apply_rom_settings, LocalRom, get_hash_string
from worlds.alttp.Rules import set_rules
from worlds.alttp.Dungeons import create_dungeons, fill_dungeons, fill_dungeons_restrictive
from Fill import distribute_items_restrictive, flood_items, balance_multiworld_progression
from Fill import distribute_items_restrictive, flood_items, balance_multiworld_progression, distribute_planned
from worlds.alttp.ItemPool import generate_itempool, difficulties, fill_prizes
from Utils import output_path, parse_player_names, get_options, __version__, _version_tuple
import Patch
@ -120,6 +120,11 @@ def main(args, seed=None):
item.strip() in item_table}
world.non_local_items[player] = {item.strip() for item in args.non_local_items[player].split(',') if
item.strip() in item_table}
# enforce pre-defined local items.
if world.goal[player] in ["localtriforcehunt", "localganontriforcehunt"]:
world.local_items[player].add('Triforce Piece')
# items can't be both local and non-local, prefer local
world.non_local_items[player] -= world.local_items[player]
@ -180,42 +185,7 @@ def main(args, seed=None):
logger.info("Running Item Plando")
world_name_lookup = {world.player_names[player_id][0]: player_id for player_id in world.player_ids}
for player in world.player_ids:
placement: PlandoItem
for placement in world.plando_items[player]:
target_world: int = placement.world
if target_world is False or world.players == 1:
target_world = player # in own world
elif target_world is True: # in any other world
target_world = player
while target_world == player:
target_world = world.random.randint(1, world.players + 1)
elif target_world is None: # any random world
target_world = world.random.randint(1, world.players + 1)
elif type(target_world) == int: # target world by player id
pass
else: # find world by name
target_world = world_name_lookup[target_world]
location = world.get_location(placement.location, target_world)
if location.item:
raise Exception(f"Cannot place item into already filled location {location}.")
item = ItemFactory(placement.item, player)
if placement.from_pool:
try:
world.itempool.remove(item)
except ValueError:
logger.warning(f"Could not remove {item} from pool as it's already missing from it.")
if location.can_fill(world.state, item, False):
world.push_item(location, item, collect=False)
location.event = True # flag location to be checked during fill
location.locked = True
logger.debug(f"Plando placed {item} at {location}")
else:
raise Exception(f"Can't place {item} at {location} due to fill condition not met.")
distribute_planned(world)
logger.info('Placing Dungeon Items.')
@ -343,6 +313,7 @@ def main(args, seed=None):
pool = concurrent.futures.ThreadPoolExecutor()
multidata_task = None
check_beatability_task = pool.submit(world.can_beat_game)
if not args.suppress_rom:
rom_futures = []
@ -427,7 +398,8 @@ def main(args, seed=None):
f.write(multidata)
multidata_task = pool.submit(write_multidata, rom_futures)
if not check_beatability_task.result():
raise Exception("Game appears unbeatable. Aborting.")
if not args.skip_playthrough:
logger.info('Calculating playthrough.')
create_playthrough(world)
@ -573,10 +545,6 @@ def create_playthrough(world):
old_world = world
world = copy_world(world)
# if we only check for beatable, we can do this sanity check first before writing down spheres
if not world.can_beat_game():
raise RuntimeError('Cannot beat game. Something went terribly wrong here!')
# get locations containing progress items
prog_locations = [location for location in world.get_filled_locations() if location.item.advancement]
state_cache = [None]

View File

@ -1337,7 +1337,7 @@ def patch_rom(world, rom, player, team, enemized):
(0x02 if 'bombs' in world.escape_assist[player] else 0x00) |
(0x04 if 'magic' in world.escape_assist[player] else 0x00))) # Escape assist
if world.goal[player] in ['pedestal', 'triforcehunt', 'localtriforcehunt']:
if world.goal[player] in ['pedestal', 'triforcehunt', 'localtriforcehunt', 'icerodhunt']:
rom.write_byte(0x18003E, 0x01) # make ganon invincible
elif world.goal[player] in ['ganontriforcehunt', 'localganontriforcehunt']:
rom.write_byte(0x18003E, 0x05) # make ganon invincible until enough triforce pieces are collected
@ -2067,14 +2067,10 @@ def write_strings(rom, world, player, team):
items_to_hint.extend(BigKeys)
local_random.shuffle(items_to_hint)
hint_count = 5 if world.shuffle[player] not in ['vanilla', 'dungeonssimple', 'dungeonsfull'] else 8
while hint_count > 0:
while hint_count > 0 and items_to_hint:
this_item = items_to_hint.pop(0)
this_location = world.find_items(this_item, player)
local_random.shuffle(this_location)
# This looks dumb but prevents hints for Skull Woods Pinball Room's key safely with any item pool.
if this_location:
if this_location[0].name == 'Skull Woods - Pinball Room':
this_location.pop(0)
if this_location:
this_hint = this_location[0].item.hint_text + ' can be found ' + hint_text(this_location[0]) + '.'
tt[hint_locations.pop(0)] = this_hint
@ -2133,6 +2129,10 @@ def write_strings(rom, world, player, team):
else:
tt['sign_ganon'] = f'You need {world.crystals_needed_for_ganon[player]} crystals to beat Ganon and ' \
f'have beaten Agahnim atop Ganons Tower'
elif world.goal[player] == "icerodhunt":
tt['sign_ganon'] = 'Go find the Ice Rod and Kill Trinexx... Ganon is invincible!'
tt['ganon_fall_in_alt'] = 'Why are you even here?\n You can\'t even hurt me! Go kill Trinexx instead.'
tt['ganon_phase_3_alt'] = 'Seriously? Go Away, I will not Die.'
else:
if world.crystals_needed_for_ganon[player] == 1:
tt['sign_ganon'] = 'You need a crystal to beat Ganon.'

View File

@ -174,8 +174,6 @@ def item_name(state, location, player):
def locality_rules(world, player):
if world.goal[player] in ["localtriforcehunt", "localganontriforcehunt"]:
world.local_items[player].add('Triforce Piece')
if world.local_items[player]:
for location in world.get_locations():
if location.player != player:

View File

@ -222,7 +222,7 @@ TavernMan_texts = [
"Helmasaur is\nthrowing a\nparty.\nI hope it's\na masquerade!",
"I'd like to\nknow Arrghus\nbetter.\nBut he won't\ncome out of\nhis shell!",
"Mothula didn't\nhave much fun\nat the party.\nHe's immune to\nspiked punch!",
"Don't set me\nup with that\nchick from\nSteve's Town.\n\n\nI'm not\ninterested in\na Blind date!",
"Don't set me\nup with that\nlady from\nSteve's Town.\n\n\nI'm not\ninterested in\na Blind date!",
"Kholdstare is\nafraid to go\nto the circus.\nHungry kids\nthought he was\ncotton candy!",
"I asked who\nVitreous' best\nfriends are.\nHe said,\n'Me, Myself,\nand Eye!'",
"Trinexx can be\na hothead or\nhe can be an\nice guy. In\nthe end, he's\na solid\nindividual!",