Merge branch 'main' into main

This commit is contained in:
Fabian Dill 2021-09-01 11:23:30 +00:00 committed by GitHub
commit ac915d00fc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 156 additions and 117 deletions

View File

@ -213,9 +213,8 @@ class MultiWorld():
except KeyError as e:
raise KeyError('No such dungeon %s for player %d' % (dungeonname, player)) from e
def get_all_state(self, keys=False) -> CollectionState:
key = f"_all_state_{keys}"
cached = getattr(self, key, None)
def get_all_state(self) -> CollectionState:
cached = getattr(self, "_all_state", None)
if cached:
return cached.copy()
@ -223,27 +222,12 @@ class MultiWorld():
for item in self.itempool:
self.worlds[item.player].collect(ret, item)
if keys:
for p in self.get_game_players("A Link to the Past"):
world = self.worlds[p]
from worlds.alttp.Items import ItemFactory
for item in ItemFactory(
['Small Key (Hyrule Castle)', 'Big Key (Eastern Palace)', 'Big Key (Desert Palace)',
'Small Key (Desert Palace)', 'Big Key (Tower of Hera)', 'Small Key (Tower of Hera)',
'Small Key (Agahnims Tower)', 'Small Key (Agahnims Tower)',
'Big Key (Palace of Darkness)'] + ['Small Key (Palace of Darkness)'] * 6 + [
'Big Key (Thieves Town)', 'Small Key (Thieves Town)', 'Big Key (Skull Woods)'] + [
'Small Key (Skull Woods)'] * 3 + ['Big Key (Swamp Palace)',
'Small Key (Swamp Palace)', 'Big Key (Ice Palace)'] + [
'Small Key (Ice Palace)'] * 2 + ['Big Key (Misery Mire)', 'Big Key (Turtle Rock)',
'Big Key (Ganons Tower)'] + [
'Small Key (Misery Mire)'] * 3 + ['Small Key (Turtle Rock)'] * 4 + [
'Small Key (Ganons Tower)'] * 4,
p):
world.collect(ret, item)
from worlds.alttp.Dungeons import get_dungeon_item_pool
for item in get_dungeon_item_pool(self):
subworld = self.worlds[item.player]
if item.name in subworld.dungeon_local_item_names:
subworld.collect(ret, item)
ret.sweep_for_events()
setattr(self, key, ret)
return ret
def get_items(self) -> list:

View File

@ -57,11 +57,12 @@ class Option(metaclass=AssembleOptions):
"""For display purposes."""
return self.get_option_name(self.value)
def get_option_name(self, value: typing.Any) -> str:
if self.autodisplayname:
return self.name_lookup[self.value].replace("_", " ").title()
@classmethod
def get_option_name(cls, value: typing.Any) -> str:
if cls.autodisplayname:
return cls.name_lookup[value].replace("_", " ").title()
else:
return self.name_lookup[self.value]
return cls.name_lookup[value]
def __int__(self) -> int:
return self.value
@ -114,7 +115,8 @@ class Toggle(Option):
def __int__(self):
return int(self.value)
def get_option_name(self, value):
@classmethod
def get_option_name(cls, value):
return ["No", "Yes"][int(value)]
class DefaultOnToggle(Toggle):

View File

@ -8,6 +8,7 @@ from pony.flask import Pony
from flask import Flask, request, redirect, url_for, render_template, Response, session, abort, send_from_directory
from flask_caching import Cache
from flask_compress import Compress
from worlds.AutoWorld import AutoWorldRegister
from .models import *
@ -81,54 +82,6 @@ def page_not_found(err):
return render_template('404.html'), 404
games_list = {
"A Link to the Past": ("The Legend of Zelda: A Link to the Past",
"""
The Legend of Zelda: A Link to the Past is an action/adventure game. Take on the role of
Link, a boy who is destined to save the land of Hyrule. Delve through three palaces and nine
dungeons on your quest to rescue the descendents of the seven wise men and defeat the evil
Ganon!"""),
"Factorio": ("Factorio",
"""
Factorio is a game about automation. You play as an engineer who has crash landed on the planet
Nauvis, an inhospitable world filled with dangerous creatures called biters. Build a factory,
research new technologies, and become more efficient in your quest to build a rocket and return home.
"""),
"Minecraft": ("Minecraft",
"""
Minecraft is a game about creativity. In a world made entirely of cubes, you explore, discover, mine,
craft, and try not to explode. Delve deep into the earth and discover abandoned mines, ancient
structures, and materials to create a portal to another world. Defeat the Ender Dragon, and claim
victory!"""),
"Subnautica": ("Subnautica",
"""
Subnautica is an undersea exploration game. Stranded on an alien world, you become infected by
an unknown bacteria. The planet's automatic quarantine will shoot you down if you try to leave.
You must find a cure for yourself, build an escape rocket, and leave the planet.
"""),
"Ocarina of Time": ("The Legend of Zelda: Ocarina of Time",
"""
The Legend of Zelda: Ocarina of Time was the first three dimensional Zelda game. Journey as
Link as he quests to fulfil his destiny. Journey across Hyrule and defeat the evil masters of
corrupted temples or seek out the pieces of the Triforce. Defeat the evil Ganondorf to become
the Hero of Time and save Hyrule!
"""),
"Super Metroid": ("Super Metroid",
"""
Samus is back in her first 16 bit adventure! Space pirates have attacked Ceres station and stolen
the last living Metroid. Go to planet Zebes and search out the abilities you will need to power
up your suit and defeat the villainous leader of the space pirates, Mother Brain.
"""),
"Risk of Rain 2": ("Risk of Rain 2",
"""
Escape a chaotic alien planet by fighting through hordes of frenzied monsters with your friends
, or on your own. Combine loot in surprising ways and master each character until you become the
havoc you feared upon your first crash landing.
""")
# "Ori and the Blind Forest": ("Ori and the Blind Forest", "Coming Soon™"),
# "Hollow Knight": ("Hollow Knight", "Coming Soon™"),
}
# Player settings pages
@app.route('/games/<string:game>/player-settings')
@ -151,7 +104,11 @@ def game_page(game):
# List of supported games
@app.route('/games')
def games():
return render_template("games/games.html", games_list=games_list)
worlds = {}
for game, world in AutoWorldRegister.world_types.items():
if not world.hidden:
worlds[game] = world.__doc__ if world.__doc__ else "No description provided."
return render_template("games/games.html", worlds=worlds)
@app.route('/tutorial/<string:game>/<string:file>/<string:lang>')

View File

@ -1,8 +1,8 @@
from flask import send_file, Response
from flask import send_file, Response, render_template
from pony.orm import select
from Patch import update_patch_data
from WebHostLib import app, Slot, Room, Seed
from WebHostLib import app, Slot, Room, Seed, cache
import zipfile
@app.route("/dl_patch/<suuid:room_id>/<int:patch_id>")
@ -68,4 +68,14 @@ def download_slot_file(room_id, player_id: int):
fname = name.rsplit("/", 1)[0]+".zip"
else:
return "Game download not supported."
return send_file(io.BytesIO(slot_data.data), as_attachment=True, attachment_filename=fname)
return send_file(io.BytesIO(slot_data.data), as_attachment=True, attachment_filename=fname)
@app.route("/templates")
@cache.cached()
def list_yaml_templates():
files = []
from worlds.AutoWorld import AutoWorldRegister
for world_name, world in AutoWorldRegister.world_types.items():
if not world.hidden:
files.append(world_name)
return render_template("templates.html", files=files)

View File

@ -10,9 +10,18 @@ target_folder = os.path.join("WebHostLib", "static", "generated")
def create():
def dictify_range(option):
data = {option.range_start: 0, option.range_end: 0, "random": 0, "random-low": 0, "random-high": 0,
option.default: 50}
notes = {
option.range_start: "minimum value",
option.range_end: "maximum value"
}
return data, notes
for game_name, world in AutoWorldRegister.world_types.items():
res = Template(open(os.path.join("WebHostLib", "templates", "options.yaml")).read()).render(
options=world.options, __version__=__version__, game=game_name, yaml_dump=yaml.dump
options=world.options, __version__=__version__, game=game_name, yaml_dump=yaml.dump,
dictify_range=dictify_range
)
with open(os.path.join(target_folder, game_name + ".yaml"), "w") as f:
@ -39,7 +48,7 @@ def create():
for sub_option_name, sub_option_id in option.options.items():
this_option["options"].append({
"name": sub_option_name,
"name": option.get_option_name(sub_option_id),
"value": sub_option_name,
})

View File

@ -9,9 +9,9 @@
{% include 'header/grassHeader.html' %}
<div id="games">
<h1>Currently Supported Games</h1>
{% for game, (display_name, description) in games_list.items() %}
<h3><a href="{{ url_for("game_page", game=game) }}">{{ display_name}}</a></h3>
<p>{{ description}}</p>
{% for game, description in worlds.items() %}
<h3><a href="{{ url_for("game_page", game=game) }}">{{ game }}</a></h3>
<p>{{ description }}</p>
{% endfor %}
</div>
{% endblock %}

View File

@ -18,7 +18,8 @@
# http://www.yamllint.com/
description: Default {{ game }} Template # Used to describe your yaml. Useful if you have multiple files
name: YourName{number} # Your name in-game. Spaces will be replaced with underscores and there is a 16 character limit
# Your name in-game. Spaces will be replaced with underscores and there is a 16 character limit
name: YourName{number}
#{player} will be replaced with the player's slot number.
#{PLAYER} will be replaced with the player's slot number if that slot number is greater than 1.
#{number} will be replaced with the counter value of the name.
@ -51,16 +52,18 @@ progression_balancing:
# - "Progressive Weapons"
# exclude_locations: # Force certain locations to never contain progression items, and always be filled with junk.
# - "Master Sword Pedestal"
{%- macro range_option(option) %}
# you can add additional values between minimum and maximum
{%- set data, notes = dictify_range(option) %}
{%- for entry, default in data.items() %}
{{ entry }}: {{ default }}{% if notes[entry] %} # {{ notes[entry] }}{% endif %}
{%- endfor -%}
{% endmacro %}
{{ game }}:
{%- for option_key, option in options.items() %}
{{ option_key }}:{% if option.__doc__ %} # {{ option.__doc__ | replace('\n', '\n#') | indent(4, first=False) }}{% endif %}
{%- if option.range_start is defined %}
# you can add additional values between minimum and maximum
{{ option.range_start }}: 0 # minimum value
{{ option.range_end }}: 0 # maximum value
random: 50
random-low: 0
random-high: 0
{{- range_option(option) -}}
{%- elif option.options -%}
{%- for suboption_option_id, sub_option_name in option.name_lookup.items() %}
{{ sub_option_name }}: {% if suboption_option_id == option.default %}50{% else %}0{% endif %}
@ -68,4 +71,5 @@ progression_balancing:
{%- else %}
{{ yaml_dump(option.default) | indent(4, first=False) }}
{%- endif -%}
{%- endfor -%}
{%- endfor %}
{% if not options %}{}{% endif %}

View File

@ -0,0 +1,21 @@
{% extends 'pageWrapper.html' %}
{% block head %}
{% include 'header/grassHeader.html' %}
<title>Option Templates (YAML)</title>
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/markdown.css") }}" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/showdown/1.9.1/showdown.min.js"
integrity="sha512-L03kznCrNOfVxOUovR6ESfCz9Gfny7gihUX/huVbQB9zjODtYpxaVtIaAkpetoiyV2eqWbvxMH9fiSv5enX7bw=="
crossorigin="anonymous"></script>
{% endblock %}
{% block body %}
<div>
<h1>Option Templates (YAML)</h1>
<ul>
{% for file in files %}
<li><a href="{{ url_for('static', filename="generated/"+file+".yaml") }}">{{ file }}</a></li>
{% endfor %}
</ul>
</div>
{% endblock %}

View File

@ -157,6 +157,8 @@ def fill_dungeons_restrictive(autoworld, world):
in_dungeon_items.sort(
key=lambda item: sort_order.get(item.type, 1) +
(5 if (item.player, item.name) in dungeon_specific else 0))
for item in in_dungeon_items:
all_state_base.remove(item)
fill_restrictive(world, all_state_base, locations, in_dungeon_items, True, True)

View File

@ -1,5 +1,6 @@
# ToDo: With shuffle_ganon option, prevent gtower from linking to an exit only location through a 2 entrance cave.
from collections import defaultdict
from worlds.alttp.OverworldGlitchRules import overworld_glitch_connections
from worlds.alttp.UnderworldGlitchRules import underworld_glitch_connections
def link_entrances(world, player):
@ -1066,9 +1067,11 @@ def link_entrances(world, player):
raise NotImplementedError(
f'{world.shuffle[player]} Shuffling not supported yet. Player {world.get_player_name(player)}')
# mandatory hybrid major glitches connections
if world.logic[player] in ['hybridglitches', 'nologic']:
underworld_glitch_connections(world, player)
if world.logic[player] in ['owglitches', 'hybridglitches', 'nologic']:
overworld_glitch_connections(world, player)
# mandatory hybrid major glitches connections
if world.logic[player] in ['hybridglitches', 'nologic']:
underworld_glitch_connections(world, player)
# check for swamp palace fix
if world.get_entrance('Dam', player).connected_region.name != 'Dam' or world.get_entrance('Swamp Palace', player).connected_region.name != 'Swamp Palace (Entrance)':
@ -1771,9 +1774,11 @@ def link_inverted_entrances(world, player):
else:
raise NotImplementedError('Shuffling not supported yet')
# mandatory hybrid major glitches connections
if world.logic[player] in ['hybridglitches', 'nologic']:
underworld_glitch_connections(world, player)
if world.logic[player] in ['owglitches', 'hybridglitches', 'nologic']:
overworld_glitch_connections(world, player)
# mandatory hybrid major glitches connections
if world.logic[player] in ['hybridglitches', 'nologic']:
underworld_glitch_connections(world, player)
# patch swamp drain
if world.get_entrance('Dam', player).connected_region.name != 'Dam' or world.get_entrance('Swamp Palace', player).connected_region.name != 'Swamp Palace (Entrance)':

View File

@ -235,24 +235,41 @@ def no_logic_rules(world, player):
create_no_logic_connections(player, world, get_mirror_offset_spots_lw(player))
def overworld_glitch_connections(world, player):
# Boots-accessible locations.
create_owg_connections(player, world, get_boots_clip_exits_lw(world.mode[player] == 'inverted'))
create_owg_connections(player, world, get_boots_clip_exits_dw(world.mode[player] == 'inverted', player))
# Glitched speed drops.
create_owg_connections(player, world, get_glitched_speed_drops_dw(world.mode[player] == 'inverted'))
# Mirror clip spots.
if world.mode[player] != 'inverted':
create_owg_connections(player, world, get_mirror_clip_spots_dw())
create_owg_connections(player, world, get_mirror_offset_spots_dw())
else:
create_owg_connections(player, world, get_mirror_offset_spots_lw(player))
def overworld_glitches_rules(world, player):
# Boots-accessible locations.
create_owg_connections(player, world, get_boots_clip_exits_lw(world.mode[player] == 'inverted'), lambda state: state.can_boots_clip_lw(player))
create_owg_connections(player, world, get_boots_clip_exits_dw(world.mode[player] == 'inverted', player), lambda state: state.can_boots_clip_dw(player))
set_owg_connection_rules(player, world, get_boots_clip_exits_lw(world.mode[player] == 'inverted'), lambda state: state.can_boots_clip_lw(player))
set_owg_connection_rules(player, world, get_boots_clip_exits_dw(world.mode[player] == 'inverted', player), lambda state: state.can_boots_clip_dw(player))
# Glitched speed drops.
create_owg_connections(player, world, get_glitched_speed_drops_dw(world.mode[player] == 'inverted'), lambda state: state.can_get_glitched_speed_dw(player))
set_owg_connection_rules(player, world, get_glitched_speed_drops_dw(world.mode[player] == 'inverted'), lambda state: state.can_get_glitched_speed_dw(player))
# Dark Death Mountain Ledge Clip Spot also accessible with mirror.
if world.mode[player] != 'inverted':
add_alternate_rule(world.get_entrance('Dark Death Mountain Ledge Clip Spot', player), lambda state: state.has('Magic Mirror', player))
# Mirror clip spots.
if world.mode[player] != 'inverted':
create_owg_connections(player, world, get_mirror_clip_spots_dw(), lambda state: state.has('Magic Mirror', player))
create_owg_connections(player, world, get_mirror_offset_spots_dw(), lambda state: state.has('Magic Mirror', player) and state.can_boots_clip_lw(player))
set_owg_connection_rules(player, world, get_mirror_clip_spots_dw(), lambda state: state.has('Magic Mirror', player))
set_owg_connection_rules(player, world, get_mirror_offset_spots_dw(), lambda state: state.has('Magic Mirror', player) and state.can_boots_clip_lw(player))
else:
create_owg_connections(player, world, get_mirror_offset_spots_lw(player), lambda state: state.has('Magic Mirror', player) and state.can_boots_clip_dw(player))
set_owg_connection_rules(player, world, get_mirror_offset_spots_lw(player), lambda state: state.has('Magic Mirror', player) and state.can_boots_clip_dw(player))
# Regions that require the boots and some other stuff.
if world.mode[player] != 'inverted':
@ -282,12 +299,16 @@ def create_no_logic_connections(player, world, connections):
parent.exits.append(connection)
connection.connect(target)
def create_owg_connections(player, world, connections, default_rule):
def create_owg_connections(player, world, connections):
for entrance, parent_region, target_region, *rule_override in connections:
parent = world.get_region(parent_region, player)
target = world.get_region(target_region, player)
connection = Entrance(player, entrance, parent)
parent.exits.append(connection)
connection.connect(target)
def set_owg_connection_rules(player, world, connections, default_rule):
for entrance, _, _, *rule_override in connections:
connection = world.get_entrance(entrance, player)
rule = rule_override[0] if len(rule_override) > 0 else default_rule
connection.access_rule = rule

View File

@ -853,7 +853,9 @@ def set_trock_key_rules(world, player):
for entrance in ['Turtle Rock Dark Room Staircase', 'Turtle Rock (Chain Chomp Room) (North)', 'Turtle Rock (Chain Chomp Room) (South)', 'Turtle Rock Pokey Room', 'Turtle Rock Big Key Door']:
set_rule(world.get_entrance(entrance, player), lambda state: False)
all_state = world.get_all_state(True)
all_state = world.get_all_state()
all_state.reachable_regions[player] = set() # wipe reachable regions so that the locked doors actually work
all_state.stale[player] = True
# Check if each of the four main regions of the dungoen can be reached. The previous code section prevents key-costing moves within the dungeon.
can_reach_back = all_state.can_reach(world.get_region('Turtle Rock (Eye Bridge)', player)) if world.can_access_trock_eyebridge[player] is None else world.can_access_trock_eyebridge[player]

View File

@ -24,6 +24,12 @@ lttp_logger = logging.getLogger("A Link to the Past")
class ALTTPWorld(World):
"""
The Legend of Zelda: A Link to the Past is an action/adventure game. Take on the role of
Link, a boy who is destined to save the land of Hyrule. Delve through three palaces and nine
dungeons on your quest to rescue the descendents of the seven wise men and defeat the evil
Ganon!
"""
game: str = "A Link to the Past"
options = alttp_options
topology_present = True
@ -192,8 +198,8 @@ class ALTTPWorld(World):
elif 'Bow' in item_name:
if state.has('Silver Bow', item.player):
return
elif state.has('Bow', item.player) and (self.world.difficulty_requirements[item.player].progressive_bow_limit >= 2
or self.world.logic[item.player] == 'noglitches'
elif state.has('Bow', item.player) and (self.world.difficulty_requirements[item.player].progressive_bow_limit >= 2
or self.world.logic[item.player] == 'noglitches'
or self.world.swordless[item.player]): # modes where silver bow is always required for ganon
return 'Silver Bow'
elif self.world.difficulty_requirements[item.player].progressive_bow_limit >= 1:
@ -206,7 +212,7 @@ class ALTTPWorld(World):
attempts = 5
world = self.world
player = self.player
all_state = world.get_all_state(keys=True)
all_state = world.get_all_state()
crystals = [self.create_item(name) for name in ['Red Pendant', 'Blue Pendant', 'Green Pendant', 'Crystal 1', 'Crystal 2', 'Crystal 3', 'Crystal 4', 'Crystal 7', 'Crystal 5', 'Crystal 6']]
crystal_locations = [world.get_location('Turtle Rock - Prize', player),
world.get_location('Eastern Palace - Prize', player),
@ -401,4 +407,4 @@ class ALttPLogic(LogicMixin):
return True
if self.world.smallkey_shuffle[player] == smallkey_shuffle.option_universal:
return self.can_buy_unlimited('Small Key (Universal)', player)
return self.prog_items[item, player] >= count
return self.prog_items[item, player] >= count

View File

@ -24,6 +24,11 @@ all_items["Evolution Trap"] = factorio_base_id - 2
class Factorio(World):
"""
Factorio is a game about automation. You play as an engineer who has crash landed on the planet
Nauvis, an inhospitable world filled with dangerous creatures called biters. Build a factory,
research new technologies, and become more efficient in your quest to build a rocket and return home.
"""
game: str = "Factorio"
static_nodes = {"automation", "logistics", "rocket-silo"}
custom_recipes = {}

View File

@ -16,6 +16,12 @@ from ..AutoWorld import World
client_version = 6
class MinecraftWorld(World):
"""
Minecraft is a game about creativity. In a world made entirely of cubes, you explore, discover, mine,
craft, and try not to explode. Delve deep into the earth and discover abandoned mines, ancient
structures, and materials to create a portal to another world. Defeat the Ender Dragon, and claim
victory!
"""
game: str = "Minecraft"
options = minecraft_options
topology_present = True
@ -47,7 +53,7 @@ class MinecraftWorld(World):
itempool = []
junk_pool = junk_weights.copy()
# Add all required progression items
for (name, num) in required_items.items():
for (name, num) in required_items.items():
itempool += [name] * num
# Add structure compasses if desired
if self.world.structure_compasses[self.player]:
@ -85,9 +91,9 @@ class MinecraftWorld(World):
def MCRegion(region_name: str, exits=[]):
ret = Region(region_name, None, region_name, self.player, self.world)
ret.locations = [MinecraftAdvancement(self.player, loc_name, loc_data.id, ret)
for loc_name, loc_data in advancement_table.items()
for loc_name, loc_data in advancement_table.items()
if loc_data.region == region_name]
for exit in exits:
for exit in exits:
ret.exits.append(Entrance(self.player, exit, ret))
return ret
@ -100,7 +106,7 @@ class MinecraftWorld(World):
with open(os.path.join(output_directory, filename), 'wb') as f:
f.write(b64encode(bytes(json.dumps(data), 'utf-8')))
def fill_slot_data(self):
def fill_slot_data(self):
slot_data = self._get_mc_data()
for option_name in minecraft_options:
option = getattr(self.world, option_name)[self.player]
@ -115,7 +121,7 @@ class MinecraftWorld(World):
item.never_exclude = True
return item
def mc_update_output(raw_data, server, port):
def mc_update_output(raw_data, server, port):
data = json.loads(b64decode(raw_data))
data['server'] = server
data['port'] = port

View File

@ -15,6 +15,11 @@ from ..AutoWorld import World
class SubnauticaWorld(World):
"""
Subnautica is an undersea exploration game. Stranded on an alien world, you become infected by
an unknown bacteria. The planet's automatic quarantine will shoot you down if you try to leave.
You must find a cure for yourself, build an escape rocket, and leave the planet.
"""
game: str = "Subnautica"
item_name_to_id = items_lookup_name_to_id
@ -53,7 +58,7 @@ class SubnauticaWorld(World):
pass
def fill_slot_data(self):
def fill_slot_data(self):
slot_data = {}
return slot_data