mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-03-24 19:13:36 -07:00
Compare commits
16 Commits
NewSoupVi-
...
adventure-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3e07f6b6d2 | ||
|
|
61e83a300b | ||
|
|
136a13aac7 | ||
|
|
2c90db9ae7 | ||
|
|
507e051a5a | ||
|
|
b5bf9ed1d7 | ||
|
|
215eb7e473 | ||
|
|
f42233699a | ||
|
|
1bec68df4d | ||
|
|
d8576e72eb | ||
|
|
7265468e8d | ||
|
|
d07f36dedd | ||
|
|
364a1b71ec | ||
|
|
daee6d210f | ||
|
|
96be0071e6 | ||
|
|
ff8e1dfb47 |
@@ -413,7 +413,8 @@ class CommonContext:
|
|||||||
await self.server.socket.close()
|
await self.server.socket.close()
|
||||||
if self.server_task is not None:
|
if self.server_task is not None:
|
||||||
await self.server_task
|
await self.server_task
|
||||||
self.ui.update_hints()
|
if self.ui:
|
||||||
|
self.ui.update_hints()
|
||||||
|
|
||||||
async def send_msgs(self, msgs: typing.List[typing.Any]) -> None:
|
async def send_msgs(self, msgs: typing.List[typing.Any]) -> None:
|
||||||
""" `msgs` JSON serializable """
|
""" `msgs` JSON serializable """
|
||||||
@@ -624,9 +625,6 @@ class CommonContext:
|
|||||||
|
|
||||||
def consume_network_data_package(self, data_package: dict):
|
def consume_network_data_package(self, data_package: dict):
|
||||||
self.update_data_package(data_package)
|
self.update_data_package(data_package)
|
||||||
current_cache = Utils.persistent_load().get("datapackage", {}).get("games", {})
|
|
||||||
current_cache.update(data_package["games"])
|
|
||||||
Utils.persistent_store("datapackage", "games", current_cache)
|
|
||||||
logger.info(f"Got new ID/Name DataPackage for {', '.join(data_package['games'])}")
|
logger.info(f"Got new ID/Name DataPackage for {', '.join(data_package['games'])}")
|
||||||
for game, game_data in data_package["games"].items():
|
for game, game_data in data_package["games"].items():
|
||||||
Utils.store_data_package_for_checksum(game, game_data)
|
Utils.store_data_package_for_checksum(game, game_data)
|
||||||
|
|||||||
2
Fill.py
2
Fill.py
@@ -348,10 +348,10 @@ def accessibility_corrections(multiworld: MultiWorld, state: CollectionState, lo
|
|||||||
if (location.item is not None and location.item.advancement and location.address is not None and not
|
if (location.item is not None and location.item.advancement and location.address is not None and not
|
||||||
location.locked and location.item.player not in minimal_players):
|
location.locked and location.item.player not in minimal_players):
|
||||||
pool.append(location.item)
|
pool.append(location.item)
|
||||||
state.remove(location.item)
|
|
||||||
location.item = None
|
location.item = None
|
||||||
if location in state.advancements:
|
if location in state.advancements:
|
||||||
state.advancements.remove(location)
|
state.advancements.remove(location)
|
||||||
|
state.remove(location.item)
|
||||||
locations.append(location)
|
locations.append(location)
|
||||||
if pool and locations:
|
if pool and locations:
|
||||||
locations.sort(key=lambda loc: loc.progress_type != LocationProgressType.PRIORITY)
|
locations.sort(key=lambda loc: loc.progress_type != LocationProgressType.PRIORITY)
|
||||||
|
|||||||
22
Generate.py
22
Generate.py
@@ -279,22 +279,30 @@ def get_choice(option, root, value=None) -> Any:
|
|||||||
raise RuntimeError(f"All options specified in \"{option}\" are weighted as zero.")
|
raise RuntimeError(f"All options specified in \"{option}\" are weighted as zero.")
|
||||||
|
|
||||||
|
|
||||||
class SafeDict(dict):
|
class SafeFormatter(string.Formatter):
|
||||||
def __missing__(self, key):
|
def get_value(self, key, args, kwargs):
|
||||||
return '{' + key + '}'
|
if isinstance(key, int):
|
||||||
|
if key < len(args):
|
||||||
|
return args[key]
|
||||||
|
else:
|
||||||
|
return "{" + str(key) + "}"
|
||||||
|
else:
|
||||||
|
return kwargs.get(key, "{" + key + "}")
|
||||||
|
|
||||||
|
|
||||||
def handle_name(name: str, player: int, name_counter: Counter):
|
def handle_name(name: str, player: int, name_counter: Counter):
|
||||||
name_counter[name.lower()] += 1
|
name_counter[name.lower()] += 1
|
||||||
number = name_counter[name.lower()]
|
number = name_counter[name.lower()]
|
||||||
new_name = "%".join([x.replace("%number%", "{number}").replace("%player%", "{player}") for x in name.split("%%")])
|
new_name = "%".join([x.replace("%number%", "{number}").replace("%player%", "{player}") for x in name.split("%%")])
|
||||||
new_name = string.Formatter().vformat(new_name, (), SafeDict(number=number,
|
|
||||||
NUMBER=(number if number > 1 else ''),
|
new_name = SafeFormatter().vformat(new_name, (), {"number": number,
|
||||||
player=player,
|
"NUMBER": (number if number > 1 else ''),
|
||||||
PLAYER=(player if player > 1 else '')))
|
"player": player,
|
||||||
|
"PLAYER": (player if player > 1 else '')})
|
||||||
# Run .strip twice for edge case where after the initial .slice new_name has a leading whitespace.
|
# Run .strip twice for edge case where after the initial .slice new_name has a leading whitespace.
|
||||||
# Could cause issues for some clients that cannot handle the additional whitespace.
|
# Could cause issues for some clients that cannot handle the additional whitespace.
|
||||||
new_name = new_name.strip()[:16].strip()
|
new_name = new_name.strip()[:16].strip()
|
||||||
|
|
||||||
if new_name == "Archipelago":
|
if new_name == "Archipelago":
|
||||||
raise Exception(f"You cannot name yourself \"{new_name}\"")
|
raise Exception(f"You cannot name yourself \"{new_name}\"")
|
||||||
return new_name
|
return new_name
|
||||||
|
|||||||
2
Utils.py
2
Utils.py
@@ -47,7 +47,7 @@ class Version(typing.NamedTuple):
|
|||||||
return ".".join(str(item) for item in self)
|
return ".".join(str(item) for item in self)
|
||||||
|
|
||||||
|
|
||||||
__version__ = "0.6.0"
|
__version__ = "0.6.2"
|
||||||
version_tuple = tuplize_version(__version__)
|
version_tuple = tuplize_version(__version__)
|
||||||
|
|
||||||
is_linux = sys.platform.startswith("linux")
|
is_linux = sys.platform.startswith("linux")
|
||||||
|
|||||||
@@ -35,6 +35,12 @@ def start_playing():
|
|||||||
@app.route('/games/<string:game>/info/<string:lang>')
|
@app.route('/games/<string:game>/info/<string:lang>')
|
||||||
@cache.cached()
|
@cache.cached()
|
||||||
def game_info(game, lang):
|
def game_info(game, lang):
|
||||||
|
try:
|
||||||
|
world = AutoWorldRegister.world_types[game]
|
||||||
|
if lang not in world.web.game_info_languages:
|
||||||
|
raise KeyError("Sorry, this game's info page is not available in that language yet.")
|
||||||
|
except KeyError:
|
||||||
|
return abort(404)
|
||||||
return render_template('gameInfo.html', game=game, lang=lang, theme=get_world_theme(game))
|
return render_template('gameInfo.html', game=game, lang=lang, theme=get_world_theme(game))
|
||||||
|
|
||||||
|
|
||||||
@@ -52,6 +58,12 @@ def games():
|
|||||||
@app.route('/tutorial/<string:game>/<string:file>/<string:lang>')
|
@app.route('/tutorial/<string:game>/<string:file>/<string:lang>')
|
||||||
@cache.cached()
|
@cache.cached()
|
||||||
def tutorial(game, file, lang):
|
def tutorial(game, file, lang):
|
||||||
|
try:
|
||||||
|
world = AutoWorldRegister.world_types[game]
|
||||||
|
if lang not in [tut.link.split("/")[1] for tut in world.web.tutorials]:
|
||||||
|
raise KeyError("Sorry, the tutorial is not available in that language yet.")
|
||||||
|
except KeyError:
|
||||||
|
return abort(404)
|
||||||
return render_template("tutorial.html", game=game, file=file, lang=lang, theme=get_world_theme(game))
|
return render_template("tutorial.html", game=game, file=file, lang=lang, theme=get_world_theme(game))
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from typing import Dict, Union
|
|||||||
from docutils.core import publish_parts
|
from docutils.core import publish_parts
|
||||||
|
|
||||||
import yaml
|
import yaml
|
||||||
from flask import redirect, render_template, request, Response
|
from flask import redirect, render_template, request, Response, abort
|
||||||
|
|
||||||
import Options
|
import Options
|
||||||
from Utils import local_path
|
from Utils import local_path
|
||||||
@@ -142,7 +142,10 @@ def weighted_options_old():
|
|||||||
@app.route("/games/<string:game>/weighted-options")
|
@app.route("/games/<string:game>/weighted-options")
|
||||||
@cache.cached()
|
@cache.cached()
|
||||||
def weighted_options(game: str):
|
def weighted_options(game: str):
|
||||||
return render_options_page("weightedOptions/weightedOptions.html", game, is_complex=True)
|
try:
|
||||||
|
return render_options_page("weightedOptions/weightedOptions.html", game, is_complex=True)
|
||||||
|
except KeyError:
|
||||||
|
return abort(404)
|
||||||
|
|
||||||
|
|
||||||
@app.route("/games/<string:game>/generate-weighted-yaml", methods=["POST"])
|
@app.route("/games/<string:game>/generate-weighted-yaml", methods=["POST"])
|
||||||
@@ -197,7 +200,10 @@ def generate_weighted_yaml(game: str):
|
|||||||
@app.route("/games/<string:game>/player-options")
|
@app.route("/games/<string:game>/player-options")
|
||||||
@cache.cached()
|
@cache.cached()
|
||||||
def player_options(game: str):
|
def player_options(game: str):
|
||||||
return render_options_page("playerOptions/playerOptions.html", game, is_complex=False)
|
try:
|
||||||
|
return render_options_page("playerOptions/playerOptions.html", game, is_complex=False)
|
||||||
|
except KeyError:
|
||||||
|
return abort(404)
|
||||||
|
|
||||||
|
|
||||||
# YAML generator for player-options
|
# YAML generator for player-options
|
||||||
|
|||||||
@@ -42,10 +42,5 @@ window.addEventListener('load', () => {
|
|||||||
scrollTarget?.scrollIntoView();
|
scrollTarget?.scrollIntoView();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}).catch((error) => {
|
|
||||||
console.error(error);
|
|
||||||
gameInfo.innerHTML =
|
|
||||||
`<h2>This page is out of logic!</h2>
|
|
||||||
<h3>Click <a href="${window.location.origin}">here</a> to return to safety.</h3>`;
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -49,10 +49,5 @@ window.addEventListener('load', () => {
|
|||||||
scrollTarget?.scrollIntoView();
|
scrollTarget?.scrollIntoView();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}).catch((error) => {
|
|
||||||
console.error(error);
|
|
||||||
tutorialWrapper.innerHTML =
|
|
||||||
`<h2>This page is out of logic!</h2>
|
|
||||||
<h3>Click <a href="${window.location.origin}/tutorial">here</a> to return to safety.</h3>`;
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
# Adding Games
|
# Adding Games
|
||||||
|
|
||||||
|
Like all contributions to Archipelago, New Game implementations should follow the [Contributing](/docs/contributing.md)
|
||||||
|
guide.
|
||||||
|
|
||||||
Adding a new game to Archipelago has two major parts:
|
Adding a new game to Archipelago has two major parts:
|
||||||
|
|
||||||
* Game Modification to communicate with Archipelago server (hereafter referred to as "client")
|
* Game Modification to communicate with Archipelago server (hereafter referred to as "client")
|
||||||
@@ -13,30 +16,51 @@ it will not be detailed here.
|
|||||||
|
|
||||||
The client is an intermediary program between the game and the Archipelago server. This can either be a direct
|
The client is an intermediary program between the game and the Archipelago server. This can either be a direct
|
||||||
modification to the game, an external program, or both. This can be implemented in nearly any modern language, but it
|
modification to the game, an external program, or both. This can be implemented in nearly any modern language, but it
|
||||||
must fulfill a few requirements in order to function as expected. The specific requirements the game client must follow
|
must fulfill a few requirements in order to function as expected. Libraries for most modern languages and the spec for
|
||||||
to behave as expected are:
|
various packets can be found in the [network protocol](/docs/network%20protocol.md) API reference document.
|
||||||
|
|
||||||
|
### Hard Requirements
|
||||||
|
|
||||||
|
In order for the game client to behave as expected, it must be able to perform these functions:
|
||||||
|
|
||||||
* Handle both secure and unsecure websocket connections
|
* Handle both secure and unsecure websocket connections
|
||||||
* Detect and react when a location has been "checked" by the player by sending a network packet to the server
|
* Reconnect if the connection is unstable and lost while playing
|
||||||
* Receive and parse network packets when the player receives an item from the server, and reward it to the player on
|
|
||||||
demand
|
|
||||||
* **Any** of your items can be received any number of times, up to and far surpassing those that the game might
|
|
||||||
normally expect from features such as starting inventory, item link replacement, or item cheating
|
|
||||||
* Players and the admin can cheat items to the player at any time with a server command, and these items may not have
|
|
||||||
a player or location attributed to them
|
|
||||||
* Be able to change the port for saved connection info
|
* Be able to change the port for saved connection info
|
||||||
* Rooms hosted on the website attempt to reserve their port, but since there are a limited number of ports, this
|
* Rooms hosted on the website attempt to reserve their port, but since there are a limited number of ports, this
|
||||||
privilege can be lost, requiring the room to be moved to a new port
|
privilege can be lost, requiring the room to be moved to a new port
|
||||||
* Reconnect if the connection is unstable and lost while playing
|
|
||||||
* Keep an index for items received in order to resync. The ItemsReceived Packets are a single list with guaranteed
|
|
||||||
order.
|
|
||||||
* Receive items that were sent to the player while they were not connected to the server
|
|
||||||
* The player being able to complete checks while offline and sending them when reconnecting is a good bonus, but not
|
|
||||||
strictly required
|
|
||||||
* Send a status update packet alerting the server that the player has completed their goal
|
* Send a status update packet alerting the server that the player has completed their goal
|
||||||
|
|
||||||
Libraries for most modern languages and the spec for various packets can be found in the
|
Regarding items and locations, the game client must be able to handle these tasks:
|
||||||
[network protocol](/docs/network%20protocol.md) API reference document.
|
|
||||||
|
#### Location Handling
|
||||||
|
|
||||||
|
Send a network packet to the server when it detects a location has been "checked" by the player in-game.
|
||||||
|
|
||||||
|
* If actions were taken in game that would usually trigger a location check, and those actions can only ever be taken
|
||||||
|
once, but the client was not connected when they happened: The client must send those location checks on connection
|
||||||
|
so that they are not permanently lost, e.g. by reading flags in the game state or save file.
|
||||||
|
|
||||||
|
#### Item Handling
|
||||||
|
|
||||||
|
Receive and parse network packets from the server when the player receives an item.
|
||||||
|
|
||||||
|
* It must reward items to the player on demand, as items can come from other players at any time.
|
||||||
|
* It must be able to reward copies of an item, up to and beyond the number the game normally expects. This may happen
|
||||||
|
due to features such as starting inventory, item link replacement, admin commands, or item cheating. **Any** of
|
||||||
|
your items can be received **any** number of times.
|
||||||
|
* Admins and players may use server commands to create items without a player or location attributed to them. The
|
||||||
|
client must be able to handle these items.
|
||||||
|
* It must keep an index for items received in order to resync. The ItemsReceived Packets are a single list with a
|
||||||
|
guaranteed order.
|
||||||
|
* It must be able to receive items that were sent to the player while they were not connected to the server.
|
||||||
|
|
||||||
|
### Encouraged Features
|
||||||
|
|
||||||
|
These are "nice to have" features for a client, but they are not strictly required. It is encouraged to add them
|
||||||
|
if possible.
|
||||||
|
|
||||||
|
* If your client appears in the Archipelago Launcher, you may define an icon for it that differentiates it from
|
||||||
|
other clients. The icon size is 38x38 pixels, but it will accept larger images with downscaling.
|
||||||
|
|
||||||
## World
|
## World
|
||||||
|
|
||||||
@@ -44,35 +68,90 @@ The world is your game integration for the Archipelago generator, webhost, and m
|
|||||||
information necessary for creating the items and locations to be randomized, the logic for item placement, the
|
information necessary for creating the items and locations to be randomized, the logic for item placement, the
|
||||||
datapackage information so other game clients can recognize your game data, and documentation. Your world must be
|
datapackage information so other game clients can recognize your game data, and documentation. Your world must be
|
||||||
written as a Python package to be loaded by Archipelago. This is currently done by creating a fork of the Archipelago
|
written as a Python package to be loaded by Archipelago. This is currently done by creating a fork of the Archipelago
|
||||||
repository and creating a new world package in `/worlds/`. A bare minimum world implementation must satisfy the
|
repository and creating a new world package in `/worlds/`.
|
||||||
following requirements:
|
|
||||||
|
|
||||||
* A folder within `/worlds/` that contains an `__init__.py`
|
The base World class can be found in [AutoWorld](/worlds/AutoWorld.py). Methods available for your world to call
|
||||||
* A `World` subclass where you create your world and define all of its rules
|
during generation can be found in [BaseClasses](/BaseClasses.py) and [Fill](/Fill.py). Some examples and documentation
|
||||||
* A unique game name
|
regarding the API can be found in the [world api doc](/docs/world%20api.md). Before publishing, make sure to also
|
||||||
* For webhost documentation and behaviors, a `WebWorld` subclass that must be instantiated in the `World` class
|
check out [world maintainer.md](/docs/world%20maintainer.md).
|
||||||
definition
|
|
||||||
* The game_info doc must follow the format `{language_code}_{game_name}.md`
|
### Hard Requirements
|
||||||
|
|
||||||
|
A bare minimum world implementation must satisfy the following requirements:
|
||||||
|
|
||||||
|
* It has a folder with the name of your game (or an abbreviation) under `/worlds/`
|
||||||
|
* The `/worlds/{game}` folder contains an `__init__.py`
|
||||||
|
* Any subfolders within `/worlds/{game}` that contain `*.py` files also contain an `__init__.py` for frozen build
|
||||||
|
packaging
|
||||||
|
* The game folder has at least one game_info doc named with follow the format `{language_code}_{game_name}.md`
|
||||||
|
* The game folder has at least one setup doc
|
||||||
|
* There must be a `World` subclass in your game folder (typically in `/worlds/{game}/__init__.py`) where you create
|
||||||
|
your world and define all of its rules and features
|
||||||
|
|
||||||
|
Within the `World` subclass you should also have:
|
||||||
|
|
||||||
|
* A [unique game name](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/AutoWorld.py#L260)
|
||||||
|
* An [instance](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/AutoWorld.py#L295) of a `WebWorld`
|
||||||
|
subclass for webhost documentation and behaviors
|
||||||
|
* In your `WebWorld`, if you wrote a game_info doc in more than one language, override the list of
|
||||||
|
[game info languages](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/AutoWorld.py#L210) with the
|
||||||
|
ones you include.
|
||||||
|
* In your `WebWorld`, override the list of
|
||||||
|
[tutorials](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/AutoWorld.py#L213) with each tutorial
|
||||||
|
or setup doc you included in the game folder.
|
||||||
* A mapping for items and locations defining their names and ids for clients to be able to identify them. These are
|
* A mapping for items and locations defining their names and ids for clients to be able to identify them. These are
|
||||||
`item_name_to_id` and `location_name_to_id`, respectively.
|
`item_name_to_id` and `location_name_to_id`, respectively.
|
||||||
* Create an item when `create_item` is called both by your code and externally
|
* An implementation of `create_item` that can create an item when called by either your code or by another process
|
||||||
* An `options_dataclass` defining the options players have available to them
|
within Archipelago
|
||||||
* A `Region` for your player with the name "Menu" to start from
|
* At least one `Region` for your player to start from (i.e. the Origin Region)
|
||||||
* Create a non-zero number of locations and add them to your regions
|
* The default name of this region is "Menu" but you may configure a different name with
|
||||||
* Create a non-zero number of items **equal** to the number of locations and add them to the multiworld itempool
|
[origin_region_name](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/AutoWorld.py#L298-L299)
|
||||||
* All items submitted to the multiworld itempool must not be manually placed by the World. If you need to place specific
|
* A non-zero number of locations, added to your regions
|
||||||
items, there are multiple ways to do so, but they should not be added to the multiworld itempool.
|
* A non-zero number of items **equal** to the number of locations, added to the multiworld itempool
|
||||||
|
* In rare cases, there may be 0-location-0-item games, but this is extremely atypical.
|
||||||
|
|
||||||
Notable caveats:
|
### Encouraged Features
|
||||||
* The "Menu" region will always be considered the "start" for the player
|
|
||||||
* The "Menu" region is *always* considered accessible; i.e. the player is expected to always be able to return to the
|
These are "nice to have" features for a world, but they are not strictly required. It is encouraged to add them
|
||||||
|
if possible.
|
||||||
|
|
||||||
|
* An implementation of
|
||||||
|
[get_filler_item_name](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/AutoWorld.py#L473)
|
||||||
|
* By default, this function chooses any item name from `item_name_to_id`, so you want to limit it to only the true
|
||||||
|
filler items.
|
||||||
|
* An `options_dataclass` defining the options players have available to them
|
||||||
|
* This should be accompanied by a type hint for `options` with the same class name
|
||||||
|
* A [bug report page](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/AutoWorld.py#L220)
|
||||||
|
* A list of [option groups](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/AutoWorld.py#L226)
|
||||||
|
for better organization on the webhost
|
||||||
|
* A dictionary of [options presets](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/AutoWorld.py#L223)
|
||||||
|
for player convenience
|
||||||
|
* A dictionary of [item name groups](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/AutoWorld.py#L273)
|
||||||
|
for player convenience
|
||||||
|
* A dictionary of
|
||||||
|
[location name groups](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/AutoWorld.py#L276)
|
||||||
|
for player convenience
|
||||||
|
* Other games may also benefit from your name group dictionaries for hints, features, etc.
|
||||||
|
|
||||||
|
### Discouraged or Prohibited Behavior
|
||||||
|
|
||||||
|
These are behaviors or implementations that are known to cause various issues. Some of these points have notable
|
||||||
|
workarounds or preferred methods which should be used instead:
|
||||||
|
|
||||||
|
* All items submitted to the multiworld itempool must not be manually placed by the World.
|
||||||
|
* If you need to place specific items, there are multiple ways to do so, but they should not be added to the
|
||||||
|
multiworld itempool.
|
||||||
|
* It is not allowed to use `eval` for most reasons, chiefly due to security concerns.
|
||||||
|
* It is discouraged to use `yaml.load` directly due to security concerns.
|
||||||
|
* When possible, use `Utils.yaml_load` instead, as this defaults to the safe loader.
|
||||||
|
* When submitting regions or items to the multiworld (`multiworld.regions` and `multiworld.itempool` respectively),
|
||||||
|
Do **not** use `=` as this will overwrite all elements for all games in the seed.
|
||||||
|
* Instead, use `append`, `extend`, or `+=`.
|
||||||
|
|
||||||
|
### Notable Caveats
|
||||||
|
|
||||||
|
* The Origin Region will always be considered the "start" for the player
|
||||||
|
* The Origin Region is *always* considered accessible; i.e. the player is expected to always be able to return to the
|
||||||
start of the game from anywhere
|
start of the game from anywhere
|
||||||
* When submitting regions or items to the multiworld (multiworld.regions and multiworld.itempool respectively), use
|
|
||||||
`append`, `extend`, or `+=`. **Do not use `=`**
|
|
||||||
* Regions are simply containers for locations that share similar access rules. They do not have to map to
|
* Regions are simply containers for locations that share similar access rules. They do not have to map to
|
||||||
concrete, physical areas within your game and can be more abstract like tech trees or a questline.
|
concrete, physical areas within your game and can be more abstract like tech trees or a questline.
|
||||||
|
|
||||||
The base World class can be found in [AutoWorld](/worlds/AutoWorld.py). Methods available for your world to call during
|
|
||||||
generation can be found in [BaseClasses](/BaseClasses.py) and [Fill](/Fill.py). Some examples and documentation
|
|
||||||
regarding the API can be found in the [world api doc](/docs/world%20api.md).
|
|
||||||
Before publishing, make sure to also check out [world maintainer.md](/docs/world%20maintainer.md).
|
|
||||||
|
|||||||
@@ -756,8 +756,8 @@ Tags are represented as a list of strings, the common client tags follow:
|
|||||||
### DeathLink
|
### DeathLink
|
||||||
A special kind of Bounce packet that can be supported by any AP game. It targets the tag "DeathLink" and carries the following data:
|
A special kind of Bounce packet that can be supported by any AP game. It targets the tag "DeathLink" and carries the following data:
|
||||||
|
|
||||||
| Name | Type | Notes |
|
| Name | Type | Notes |
|
||||||
|--------|-------|--------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|--------|-------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| time | float | Unix Time Stamp of time of death. |
|
| time | float | Unix Time Stamp of time of death. |
|
||||||
| cause | str | Optional. Text to explain the cause of death. When provided, or checked, this should contain the player name, ex. "Berserker was run over by a train." |
|
| cause | str | Optional. Text to explain the cause of death. When provided, or checked, if the string is non-empty, it should contain the player name, ex. "Berserker was run over by a train." |
|
||||||
| source | str | Name of the player who first died. Can be a slot name, but can also be a name from within a multiplayer game. |
|
| source | str | Name of the player who first died. Can be a slot name, but can also be a name from within a multiplayer game. |
|
||||||
|
|||||||
2
kvui.py
2
kvui.py
@@ -296,7 +296,7 @@ class SelectableLabel(RecycleDataViewBehavior, TooltipLabel):
|
|||||||
else:
|
else:
|
||||||
# Not a fan of the following few lines, but they work.
|
# Not a fan of the following few lines, but they work.
|
||||||
temp = MarkupLabel(text=self.text).markup
|
temp = MarkupLabel(text=self.text).markup
|
||||||
text = "".join(part for part in temp if not part.startswith(("[color", "[/color]", "[ref=", "[/ref]")))
|
text = "".join(part for part in temp if not part.startswith("["))
|
||||||
cmdinput = App.get_running_app().textinput
|
cmdinput = App.get_running_app().textinput
|
||||||
if not cmdinput.text:
|
if not cmdinput.text:
|
||||||
input_text = get_input_text_from_response(text, App.get_running_app().last_autofillable_command)
|
input_text = get_input_text_from_response(text, App.get_running_app().last_autofillable_command)
|
||||||
|
|||||||
@@ -88,7 +88,6 @@ processes = weakref.WeakSet()
|
|||||||
|
|
||||||
|
|
||||||
def launch_subprocess(func: Callable, name: str | None = None, args: Tuple[str, ...] = ()) -> None:
|
def launch_subprocess(func: Callable, name: str | None = None, args: Tuple[str, ...] = ()) -> None:
|
||||||
global processes
|
|
||||||
import multiprocessing
|
import multiprocessing
|
||||||
process = multiprocessing.Process(target=func, name=name, args=args)
|
process = multiprocessing.Process(target=func, name=name, args=args)
|
||||||
process.start()
|
process.start()
|
||||||
|
|||||||
@@ -238,14 +238,12 @@ class AdventureWorld(World):
|
|||||||
|
|
||||||
def create_regions(self) -> None:
|
def create_regions(self) -> None:
|
||||||
create_regions(self.options, self.multiworld, self.player, self.dragon_rooms)
|
create_regions(self.options, self.multiworld, self.player, self.dragon_rooms)
|
||||||
|
|
||||||
set_rules = set_rules
|
|
||||||
|
|
||||||
def generate_basic(self) -> None:
|
|
||||||
self.multiworld.get_location("Chalice Home", self.player).place_locked_item(
|
self.multiworld.get_location("Chalice Home", self.player).place_locked_item(
|
||||||
self.create_event("Victory", ItemClassification.progression))
|
self.create_event("Victory", ItemClassification.progression))
|
||||||
self.multiworld.completion_condition[self.player] = lambda state: state.has("Victory", self.player)
|
self.multiworld.completion_condition[self.player] = lambda state: state.has("Victory", self.player)
|
||||||
|
|
||||||
|
set_rules = set_rules
|
||||||
|
|
||||||
def pre_fill(self):
|
def pre_fill(self):
|
||||||
# Place empty items in filler locations here, to limit
|
# Place empty items in filler locations here, to limit
|
||||||
# the number of exported empty items and the density of stuff in overworld.
|
# the number of exported empty items and the density of stuff in overworld.
|
||||||
|
|||||||
@@ -121,6 +121,7 @@ class ALTTPWeb(WebWorld):
|
|||||||
)
|
)
|
||||||
|
|
||||||
tutorials = [setup_en, setup_de, setup_es, setup_fr, msu, msu_es, msu_fr, plando, oof_sound]
|
tutorials = [setup_en, setup_de, setup_es, setup_fr, msu, msu_es, msu_fr, plando, oof_sound]
|
||||||
|
game_info_languages = ["en", "fr"]
|
||||||
|
|
||||||
|
|
||||||
class ALTTPWorld(World):
|
class ALTTPWorld(World):
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ class AquariaWeb(WebWorld):
|
|||||||
)
|
)
|
||||||
|
|
||||||
tutorials = [setup, setup_fr]
|
tutorials = [setup, setup_fr]
|
||||||
|
game_info_languages = ["en", "fr"]
|
||||||
|
|
||||||
|
|
||||||
class AquariaWorld(World):
|
class AquariaWorld(World):
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ class CliqueWebWorld(WebWorld):
|
|||||||
)
|
)
|
||||||
|
|
||||||
tutorials = [setup_en, setup_de]
|
tutorials = [setup_en, setup_de]
|
||||||
|
game_info_languages = ["en", "de"]
|
||||||
|
|
||||||
|
|
||||||
class CliqueWorld(World):
|
class CliqueWorld(World):
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ class DLCqwebworld(WebWorld):
|
|||||||
["Deoxis"]
|
["Deoxis"]
|
||||||
)
|
)
|
||||||
tutorials = [setup_en, setup_fr]
|
tutorials = [setup_en, setup_fr]
|
||||||
|
game_info_languages = ["en", "fr"]
|
||||||
|
|
||||||
|
|
||||||
class DLCqworld(World):
|
class DLCqworld(World):
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ class FFMQWebWorld(WebWorld):
|
|||||||
)
|
)
|
||||||
|
|
||||||
tutorials = [setup_en, setup_fr]
|
tutorials = [setup_en, setup_fr]
|
||||||
|
game_info_languages = ["en", "fr"]
|
||||||
|
|
||||||
|
|
||||||
class FFMQWorld(World):
|
class FFMQWorld(World):
|
||||||
|
|||||||
@@ -130,6 +130,7 @@ class OOTWeb(WebWorld):
|
|||||||
|
|
||||||
tutorials = [setup, setup_fr, setup_de]
|
tutorials = [setup, setup_fr, setup_de]
|
||||||
option_groups = oot_option_groups
|
option_groups = oot_option_groups
|
||||||
|
game_info_languages = ["en", "de"]
|
||||||
|
|
||||||
|
|
||||||
class OOTWorld(World):
|
class OOTWorld(World):
|
||||||
|
|||||||
@@ -1,3 +1,11 @@
|
|||||||
|
# 2.4.1
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
|
||||||
|
- Fixed handling of shuffle option for badges/HMs in the case that the player sets those items to nonlocal or uses
|
||||||
|
plando to put an item in one of those locations, or in the case that fill gets itself stuck on these items and has to
|
||||||
|
retry.
|
||||||
|
|
||||||
# 2.4.0
|
# 2.4.0
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
@@ -14,9 +22,6 @@ _not_ used for logical access (the seed will never require you to catch somethin
|
|||||||
|
|
||||||
- Now excludes the location "Navel Rock Top - Hidden Item Sacred Ash" if your goal is Champion and you didn't randomize
|
- Now excludes the location "Navel Rock Top - Hidden Item Sacred Ash" if your goal is Champion and you didn't randomize
|
||||||
event tickets.
|
event tickets.
|
||||||
- Fixed handling of shuffle option for badges/HMs in the case that the player sets those items to nonlocal or uses
|
|
||||||
plando to put an item in one of those locations, or in the case that fill gets itself stuck on these items and has to
|
|
||||||
retry.
|
|
||||||
|
|
||||||
# 2.3.0
|
# 2.3.0
|
||||||
|
|
||||||
|
|||||||
@@ -2414,6 +2414,7 @@ def door_shuffle(world, multiworld, player, badges, badge_locs):
|
|||||||
loc.place_locked_item(badge)
|
loc.place_locked_item(badge)
|
||||||
|
|
||||||
state = multiworld.state.copy()
|
state = multiworld.state.copy()
|
||||||
|
state.allow_partial_entrances = True
|
||||||
for item, data in item_table.items():
|
for item, data in item_table.items():
|
||||||
if (data.id or item in poke_data.pokemon_data) and data.classification == ItemClassification.progression \
|
if (data.id or item in poke_data.pokemon_data) and data.classification == ItemClassification.progression \
|
||||||
and ("Badge" not in item or world.options.badgesanity):
|
and ("Badge" not in item or world.options.badgesanity):
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ class Starcraft2WebWorld(WebWorld):
|
|||||||
)
|
)
|
||||||
|
|
||||||
tutorials = [setup_en, setup_fr]
|
tutorials = [setup_en, setup_fr]
|
||||||
|
game_info_languages = ["en", "fr"]
|
||||||
|
|
||||||
|
|
||||||
class SC2World(World):
|
class SC2World(World):
|
||||||
|
|||||||
@@ -88,6 +88,8 @@ Notes:
|
|||||||
See the [Archipelago Plando Guide](../../../tutorial/Archipelago/plando/en) for more information on Plando and Connection Plando.
|
See the [Archipelago Plando Guide](../../../tutorial/Archipelago/plando/en) for more information on Plando and Connection Plando.
|
||||||
|
|
||||||
## Is there anything else I should know?
|
## Is there anything else I should know?
|
||||||
You can go to [The TUNIC Randomizer Website](https://rando.tunic.run/) for a list of randomizer features as well as some helpful tips.
|
- You can go to [The TUNIC Randomizer Website](https://rando.tunic.run/) for a list of randomizer features as well as some helpful tips.
|
||||||
You can use the Fairy Seeking Spell (ULU RDR) to locate the nearest unchecked location.
|
- You can use the Fairy Seeking Spell (ULU RDR) to locate the nearest unchecked location.
|
||||||
You can use the Entrance Seeking Spell (RDR ULU) to locate the nearest unused entrance.
|
- You can use the Entrance Seeking Spell (RDR ULU) to locate the nearest unused entrance.
|
||||||
|
- Death Link can be toggled in game, and it can be set to receive traps instead of deaths.
|
||||||
|
- Trap Link can be toggled in-game as well, which makes it so other players with Trap Link enabled will share the effects of traps with you, and vice versa. Trap Link functions cross-game, but only with other games that have Trap Link implemented, and only some traps can be shared, depending on the game.
|
||||||
|
|||||||
@@ -330,7 +330,11 @@ def pair_portals(world: "TunicWorld", regions: Dict[str, Region]) -> Dict[Portal
|
|||||||
else:
|
else:
|
||||||
if not portal2:
|
if not portal2:
|
||||||
raise Exception(f"Could not find entrance named {p_exit} for "
|
raise Exception(f"Could not find entrance named {p_exit} for "
|
||||||
f"plando connections in {player_name}'s YAML.")
|
f"plando connections in {player_name}'s YAML.\n"
|
||||||
|
f"If you are using Universal Tracker, the most likely reason for this error "
|
||||||
|
f"is that the host generated with a newer version of the APWorld.\n"
|
||||||
|
f"Please check the TUNIC Randomizer Github and place the newest APWorld in your "
|
||||||
|
f"custom_worlds folder, and remove the one in lib/worlds if there is one there.")
|
||||||
dead_ends.remove(portal2)
|
dead_ends.remove(portal2)
|
||||||
|
|
||||||
# update the traversal chart to say you can get from portal1's region to portal2's and vice versa
|
# update the traversal chart to say you can get from portal1's region to portal2's and vice versa
|
||||||
|
|||||||
Reference in New Issue
Block a user