mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-03-12 02:23:47 -07:00
Compare commits
44 Commits
webhost_qu
...
NewSoupVi-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e7632670a6 | ||
|
|
65cf8080b1 | ||
|
|
45eef7d097 | ||
|
|
fc404d0cf7 | ||
|
|
5ce71db048 | ||
|
|
aff98a5b78 | ||
|
|
30cedb13f3 | ||
|
|
0c1ecf7297 | ||
|
|
5390561b58 | ||
|
|
bb457b0f73 | ||
|
|
6276ccf415 | ||
|
|
d3588a057c | ||
|
|
30ce74d6d5 | ||
|
|
ff59b86335 | ||
|
|
e355d20063 | ||
|
|
28ea2444a4 | ||
|
|
e907980ff0 | ||
|
|
5a933a160a | ||
|
|
c7978bcc12 | ||
|
|
5c7a84748b | ||
|
|
8dc9719b99 | ||
|
|
60617c682e | ||
|
|
fd879408f3 | ||
|
|
8decde0370 | ||
|
|
adb5a7d632 | ||
|
|
f07fea2771 | ||
|
|
a2460b7fe7 | ||
|
|
f8f30f41b7 | ||
|
|
60070c2f1e | ||
|
|
3eb25a59dc | ||
|
|
1cbc5d6649 | ||
|
|
bdef410eb2 | ||
|
|
ec9145e61d | ||
|
|
a547c8dd7d | ||
|
|
7996fd8d19 | ||
|
|
7a652518a3 | ||
|
|
ae4426af08 | ||
|
|
91e97b68d4 | ||
|
|
6a08064a52 | ||
|
|
83cfb803a7 | ||
|
|
6d7abb3780 | ||
|
|
50f6cf04f6 | ||
|
|
26c1e9b8c3 | ||
|
|
6bca1cbdac |
24
.run/Build APWorld.run.xml
Normal file
24
.run/Build APWorld.run.xml
Normal file
@@ -0,0 +1,24 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Build APWorld" type="PythonConfigurationType" factoryName="Python">
|
||||
<module name="Archipelago" />
|
||||
<option name="ENV_FILES" value="" />
|
||||
<option name="INTERPRETER_OPTIONS" value="" />
|
||||
<option name="PARENT_ENVS" value="true" />
|
||||
<envs>
|
||||
<env name="PYTHONUNBUFFERED" value="1" />
|
||||
</envs>
|
||||
<option name="SDK_HOME" value="" />
|
||||
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/" />
|
||||
<option name="IS_MODULE_SDK" value="true" />
|
||||
<option name="ADD_CONTENT_ROOTS" value="true" />
|
||||
<option name="ADD_SOURCE_ROOTS" value="true" />
|
||||
<option name="SCRIPT_NAME" value="$ContentRoot$/Launcher.py" />
|
||||
<option name="PARAMETERS" value="\"Build APWorlds\"" />
|
||||
<option name="SHOW_COMMAND_LINE" value="false" />
|
||||
<option name="EMULATE_TERMINAL" value="false" />
|
||||
<option name="MODULE_MODE" value="false" />
|
||||
<option name="REDIRECT_INPUT" value="false" />
|
||||
<option name="INPUT_FILE" value="" />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
||||
@@ -1346,8 +1346,7 @@ class Region:
|
||||
for entrance in self.entrances: # BFS might be better here, trying DFS for now.
|
||||
return entrance.parent_region.get_connecting_entrance(is_main_entrance)
|
||||
|
||||
def add_locations(self, locations: Dict[str, Optional[int]],
|
||||
location_type: Optional[type[Location]] = None) -> None:
|
||||
def add_locations(self, locations: Mapping[str, int | None], location_type: type[Location] | None = None) -> None:
|
||||
"""
|
||||
Adds locations to the Region object, where location_type is your Location class and locations is a dict of
|
||||
location names to address.
|
||||
@@ -1435,8 +1434,8 @@ class Region:
|
||||
entrance.connect(self)
|
||||
return entrance
|
||||
|
||||
def add_exits(self, exits: Union[Iterable[str], Dict[str, Optional[str]]],
|
||||
rules: Dict[str, Callable[[CollectionState], bool]] = None) -> List[Entrance]:
|
||||
def add_exits(self, exits: Iterable[str] | Mapping[str, str | None],
|
||||
rules: Mapping[str, Callable[[CollectionState], bool]] | None = None) -> List[Entrance]:
|
||||
"""
|
||||
Connects current region to regions in exit dictionary. Passed region names must exist first.
|
||||
|
||||
@@ -1444,7 +1443,7 @@ class Region:
|
||||
created entrances will be named "self.name -> connecting_region"
|
||||
:param rules: rules for the exits from this region. format is {"connecting_region": rule}
|
||||
"""
|
||||
if not isinstance(exits, Dict):
|
||||
if not isinstance(exits, Mapping):
|
||||
exits = dict.fromkeys(exits)
|
||||
return [
|
||||
self.connect(
|
||||
@@ -1858,6 +1857,9 @@ class Spoiler:
|
||||
Utils.__version__, self.multiworld.seed))
|
||||
outfile.write('Filling Algorithm: %s\n' % self.multiworld.algorithm)
|
||||
outfile.write('Players: %d\n' % self.multiworld.players)
|
||||
if self.multiworld.players > 1:
|
||||
loc_count = len([loc for loc in self.multiworld.get_locations() if not loc.is_event])
|
||||
outfile.write('Total Location Count: %d\n' % loc_count)
|
||||
outfile.write(f'Plando Options: {self.multiworld.plando_options}\n')
|
||||
AutoWorld.call_stage(self.multiworld, "write_spoiler_header", outfile)
|
||||
|
||||
@@ -1866,6 +1868,9 @@ class Spoiler:
|
||||
outfile.write('\nPlayer %d: %s\n' % (player, self.multiworld.get_player_name(player)))
|
||||
outfile.write('Game: %s\n' % self.multiworld.game[player])
|
||||
|
||||
loc_count = len([loc for loc in self.multiworld.get_locations(player) if not loc.is_event])
|
||||
outfile.write('Location Count: %d\n' % loc_count)
|
||||
|
||||
for f_option, option in self.multiworld.worlds[player].options_dataclass.type_hints.items():
|
||||
write_option(f_option, option)
|
||||
|
||||
|
||||
@@ -856,9 +856,9 @@ async def server_loop(ctx: CommonContext, address: typing.Optional[str] = None)
|
||||
|
||||
server_url = urllib.parse.urlparse(address)
|
||||
if server_url.username:
|
||||
ctx.username = server_url.username
|
||||
ctx.username = urllib.parse.unquote(server_url.username)
|
||||
if server_url.password:
|
||||
ctx.password = server_url.password
|
||||
ctx.password = urllib.parse.unquote(server_url.password)
|
||||
|
||||
def reconnect_hint() -> str:
|
||||
return ", type /connect to reconnect" if ctx.server_address else ""
|
||||
|
||||
43
Fill.py
43
Fill.py
@@ -129,6 +129,10 @@ def fill_restrictive(multiworld: MultiWorld, base_state: CollectionState, locati
|
||||
for i, location in enumerate(placements))
|
||||
for (i, location, unsafe) in swap_attempts:
|
||||
placed_item = location.item
|
||||
if item_to_place == placed_item:
|
||||
# The number of allowed swaps is limited, so do not allow a swap of an item with a copy of
|
||||
# itself.
|
||||
continue
|
||||
# Unplaceable items can sometimes be swapped infinitely. Limit the
|
||||
# number of times we will swap an individual item to prevent this
|
||||
swap_count = swapped_items[placed_item.player, placed_item.name, unsafe]
|
||||
@@ -206,12 +210,43 @@ def fill_restrictive(multiworld: MultiWorld, base_state: CollectionState, locati
|
||||
_log_fill_progress(name, placed, total)
|
||||
|
||||
if cleanup_required:
|
||||
relevant_locations = multiworld.get_filled_locations(item.player if single_player_placement else None)
|
||||
|
||||
# validate all placements and remove invalid ones
|
||||
state = sweep_from_pool(
|
||||
base_state, [], multiworld.get_filled_locations(item.player)
|
||||
if single_player_placement else None)
|
||||
cleanup_state = sweep_from_pool(base_state, [], relevant_locations)
|
||||
|
||||
# accessibilty_corrections can clean up any case where locations are unreachable as a result of
|
||||
# a full player's item being on a minimal player's unreachable location.
|
||||
# So, we make a state where we collect all such minimal->full items to check against.
|
||||
changed = False
|
||||
for location in relevant_locations:
|
||||
if location.item is None:
|
||||
continue
|
||||
if location in cleanup_state.locations_checked:
|
||||
continue
|
||||
|
||||
if multiworld.worlds[location.player].options.accessibility == "minimal":
|
||||
if multiworld.worlds[location.item.player].options.accessibility != "minimal":
|
||||
changed |= cleanup_state.collect(location.item, prevent_sweep=True)
|
||||
if changed:
|
||||
cleanup_state.sweep_for_advancements(relevant_locations)
|
||||
|
||||
for placement in placements:
|
||||
if multiworld.worlds[placement.item.player].options.accessibility != "minimal" and not placement.can_reach(state):
|
||||
# If the item's player is minimal, we don't care that it's unreachable.
|
||||
if multiworld.worlds[placement.item.player].options.accessibility == "minimal":
|
||||
continue
|
||||
|
||||
# This item belongs to a full player.
|
||||
# If the location's player is minimal, we don't need to be concerned.
|
||||
# Even if the location is inaccessible, accessibility_corrections will clean this up.
|
||||
if multiworld.worlds[placement.player].options.accessibility == "minimal":
|
||||
continue
|
||||
|
||||
# This is a full player's item on a full player's location.
|
||||
# If this item is unreachable, we have a problem - UNLESS the location is just stuck behind a full player's
|
||||
# item on a minimal player's location. That case will transitively get solved by accessibility_corrections.
|
||||
# This is why we use our special "cleanup_state", not just the maximum exploration state.
|
||||
if not placement.can_reach(cleanup_state):
|
||||
placement.item.location = None
|
||||
unplaced_items.append(placement.item)
|
||||
placement.item = None
|
||||
|
||||
9
Main.py
9
Main.py
@@ -54,13 +54,16 @@ def main(args, seed=None, baked_server_options: dict[str, object] | None = None)
|
||||
logger.info(f"Found {len(AutoWorld.AutoWorldRegister.world_types)} World Types:")
|
||||
longest_name = max(len(text) for text in AutoWorld.AutoWorldRegister.world_types)
|
||||
|
||||
item_count = len(str(max(len(cls.item_names) for cls in AutoWorld.AutoWorldRegister.world_types.values())))
|
||||
location_count = len(str(max(len(cls.location_names) for cls in AutoWorld.AutoWorldRegister.world_types.values())))
|
||||
world_classes = AutoWorld.AutoWorldRegister.world_types.values()
|
||||
|
||||
version_count = max(len(cls.world_version.as_simple_string()) for cls in world_classes)
|
||||
item_count = len(str(max(len(cls.item_names) for cls in world_classes)))
|
||||
location_count = len(str(max(len(cls.location_names) for cls in world_classes)))
|
||||
|
||||
for name, cls in AutoWorld.AutoWorldRegister.world_types.items():
|
||||
if not cls.hidden and len(cls.item_names) > 0:
|
||||
logger.info(f" {name:{longest_name}}: "
|
||||
f"v{cls.world_version.as_simple_string()} |"
|
||||
f"v{cls.world_version.as_simple_string():{version_count}} | "
|
||||
f"Items: {len(cls.item_names):{item_count}} | "
|
||||
f"Locations: {len(cls.location_names):{location_count}}")
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ if typing.TYPE_CHECKING:
|
||||
|
||||
import colorama
|
||||
import websockets
|
||||
from websockets.extensions.permessage_deflate import PerMessageDeflate
|
||||
from websockets.extensions.permessage_deflate import PerMessageDeflate, ServerPerMessageDeflateFactory
|
||||
try:
|
||||
# ponyorm is a requirement for webhost, not default server, so may not be importable
|
||||
from pony.orm.dbapiprovider import OperationalError
|
||||
@@ -50,6 +50,15 @@ from BaseClasses import ItemClassification
|
||||
min_client_version = Version(0, 5, 0)
|
||||
colorama.just_fix_windows_console()
|
||||
|
||||
no_version = Version(0, 0, 0)
|
||||
assert isinstance(no_version, tuple) # assert immutable
|
||||
|
||||
server_per_message_deflate_factory = ServerPerMessageDeflateFactory(
|
||||
server_max_window_bits=11,
|
||||
client_max_window_bits=11,
|
||||
compress_settings={"memLevel": 4},
|
||||
)
|
||||
|
||||
|
||||
def remove_from_list(container, value):
|
||||
try:
|
||||
@@ -125,8 +134,31 @@ def get_saving_second(seed_name: str, interval: int = 60) -> int:
|
||||
|
||||
|
||||
class Client(Endpoint):
|
||||
version = Version(0, 0, 0)
|
||||
tags: typing.List[str]
|
||||
__slots__ = (
|
||||
"__weakref__",
|
||||
"version",
|
||||
"auth",
|
||||
"team",
|
||||
"slot",
|
||||
"send_index",
|
||||
"tags",
|
||||
"messageprocessor",
|
||||
"ctx",
|
||||
"remote_items",
|
||||
"remote_start_inventory",
|
||||
"no_items",
|
||||
"no_locations",
|
||||
"no_text",
|
||||
)
|
||||
|
||||
version: Version
|
||||
auth: bool
|
||||
team: int | None
|
||||
slot: int | None
|
||||
send_index: int
|
||||
tags: list[str]
|
||||
messageprocessor: ClientMessageProcessor
|
||||
ctx: weakref.ref[Context]
|
||||
remote_items: bool
|
||||
remote_start_inventory: bool
|
||||
no_items: bool
|
||||
@@ -135,6 +167,7 @@ class Client(Endpoint):
|
||||
|
||||
def __init__(self, socket: "ServerConnection", ctx: Context) -> None:
|
||||
super().__init__(socket)
|
||||
self.version = no_version
|
||||
self.auth = False
|
||||
self.team = None
|
||||
self.slot = None
|
||||
@@ -142,6 +175,11 @@ class Client(Endpoint):
|
||||
self.tags = []
|
||||
self.messageprocessor = client_message_processor(ctx, self)
|
||||
self.ctx = weakref.ref(ctx)
|
||||
self.remote_items = False
|
||||
self.remote_start_inventory = False
|
||||
self.no_items = False
|
||||
self.no_locations = False
|
||||
self.no_text = False
|
||||
|
||||
@property
|
||||
def items_handling(self):
|
||||
@@ -179,6 +217,7 @@ class Context:
|
||||
"release_mode": str,
|
||||
"remaining_mode": str,
|
||||
"collect_mode": str,
|
||||
"countdown_mode": str,
|
||||
"item_cheat": bool,
|
||||
"compatibility": int}
|
||||
# team -> slot id -> list of clients authenticated to slot.
|
||||
@@ -208,8 +247,8 @@ class Context:
|
||||
|
||||
def __init__(self, host: str, port: int, server_password: str, password: str, location_check_points: int,
|
||||
hint_cost: int, item_cheat: bool, release_mode: str = "disabled", collect_mode="disabled",
|
||||
remaining_mode: str = "disabled", auto_shutdown: typing.SupportsFloat = 0, compatibility: int = 2,
|
||||
log_network: bool = False, logger: logging.Logger = logging.getLogger()):
|
||||
countdown_mode: str = "auto", remaining_mode: str = "disabled", auto_shutdown: typing.SupportsFloat = 0,
|
||||
compatibility: int = 2, log_network: bool = False, logger: logging.Logger = logging.getLogger()):
|
||||
self.logger = logger
|
||||
super(Context, self).__init__()
|
||||
self.slot_info = {}
|
||||
@@ -242,6 +281,7 @@ class Context:
|
||||
self.release_mode: str = release_mode
|
||||
self.remaining_mode: str = remaining_mode
|
||||
self.collect_mode: str = collect_mode
|
||||
self.countdown_mode: str = countdown_mode
|
||||
self.item_cheat = item_cheat
|
||||
self.exit_event = asyncio.Event()
|
||||
self.client_activity_timers: typing.Dict[
|
||||
@@ -627,6 +667,7 @@ class Context:
|
||||
"server_password": self.server_password, "password": self.password,
|
||||
"release_mode": self.release_mode,
|
||||
"remaining_mode": self.remaining_mode, "collect_mode": self.collect_mode,
|
||||
"countdown_mode": self.countdown_mode,
|
||||
"item_cheat": self.item_cheat, "compatibility": self.compatibility}
|
||||
|
||||
}
|
||||
@@ -661,6 +702,7 @@ class Context:
|
||||
self.release_mode = savedata["game_options"]["release_mode"]
|
||||
self.remaining_mode = savedata["game_options"]["remaining_mode"]
|
||||
self.collect_mode = savedata["game_options"]["collect_mode"]
|
||||
self.countdown_mode = savedata["game_options"].get("countdown_mode", self.countdown_mode)
|
||||
self.item_cheat = savedata["game_options"]["item_cheat"]
|
||||
self.compatibility = savedata["game_options"]["compatibility"]
|
||||
|
||||
@@ -1492,6 +1534,23 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
||||
" You can ask the server admin for a /collect")
|
||||
return False
|
||||
|
||||
def _cmd_countdown(self, seconds: str = "10") -> bool:
|
||||
"""Start a countdown in seconds"""
|
||||
if self.ctx.countdown_mode == "disabled" or \
|
||||
self.ctx.countdown_mode == "auto" and len(self.ctx.player_names) >= 30:
|
||||
self.output("Sorry, client countdowns have been disabled on this server. You can ask the server admin for a /countdown")
|
||||
return False
|
||||
try:
|
||||
timer = int(seconds, 10)
|
||||
except ValueError:
|
||||
timer = 10
|
||||
else:
|
||||
if timer > 60 * 60:
|
||||
raise ValueError(f"{timer} is invalid. Maximum is 1 hour.")
|
||||
|
||||
async_start(countdown(self.ctx, timer))
|
||||
return True
|
||||
|
||||
def _cmd_remaining(self) -> bool:
|
||||
"""List remaining items in your game, but not their location or recipient"""
|
||||
if self.ctx.remaining_mode == "enabled":
|
||||
@@ -2452,6 +2511,11 @@ class ServerCommandProcessor(CommonCommandProcessor):
|
||||
elif value_type == str and option_name.endswith("password"):
|
||||
def value_type(input_text: str):
|
||||
return None if input_text.lower() in {"null", "none", '""', "''"} else input_text
|
||||
elif option_name == "countdown_mode":
|
||||
valid_values = {"enabled", "disabled", "auto"}
|
||||
if option_value.lower() not in valid_values:
|
||||
self.output(f"Unrecognized {option_name} value '{option_value}', known: {', '.join(valid_values)}")
|
||||
return False
|
||||
elif value_type == str and option_name.endswith("mode"):
|
||||
valid_values = {"goal", "enabled", "disabled"}
|
||||
valid_values.update(("auto", "auto_enabled") if option_name != "remaining_mode" else [])
|
||||
@@ -2539,6 +2603,13 @@ def parse_args() -> argparse.Namespace:
|
||||
goal: !collect can be used after goal completion
|
||||
auto-enabled: !collect is available and automatically triggered on goal completion
|
||||
''')
|
||||
parser.add_argument('--countdown_mode', default=defaults["countdown_mode"], nargs='?',
|
||||
choices=['enabled', 'disabled', "auto"], help='''\
|
||||
Select !countdown Accessibility. (default: %(default)s)
|
||||
enabled: !countdown is always available
|
||||
disabled: !countdown is never available
|
||||
auto: !countdown is available for rooms with less than 30 players
|
||||
''')
|
||||
parser.add_argument('--remaining_mode', default=defaults["remaining_mode"], nargs='?',
|
||||
choices=['enabled', 'disabled', "goal"], help='''\
|
||||
Select !remaining Accessibility. (default: %(default)s)
|
||||
@@ -2604,7 +2675,7 @@ async def main(args: argparse.Namespace):
|
||||
|
||||
ctx = Context(args.host, args.port, args.server_password, args.password, args.location_check_points,
|
||||
args.hint_cost, not args.disable_item_cheat, args.release_mode, args.collect_mode,
|
||||
args.remaining_mode,
|
||||
args.countdown_mode, args.remaining_mode,
|
||||
args.auto_shutdown, args.compatibility, args.log_network)
|
||||
data_filename = args.multidata
|
||||
|
||||
@@ -2639,7 +2710,13 @@ async def main(args: argparse.Namespace):
|
||||
|
||||
ssl_context = load_server_cert(args.cert, args.cert_key) if args.cert else None
|
||||
|
||||
ctx.server = websockets.serve(functools.partial(server, ctx=ctx), host=ctx.host, port=ctx.port, ssl=ssl_context)
|
||||
ctx.server = websockets.serve(
|
||||
functools.partial(server, ctx=ctx),
|
||||
host=ctx.host,
|
||||
port=ctx.port,
|
||||
ssl=ssl_context,
|
||||
extensions=[server_per_message_deflate_factory],
|
||||
)
|
||||
ip = args.host if args.host else Utils.get_public_ipv4()
|
||||
logging.info('Hosting game at %s:%d (%s)' % (ip, ctx.port,
|
||||
'No password' if not ctx.password else 'Password: %s' % ctx.password))
|
||||
|
||||
@@ -174,6 +174,8 @@ decode = JSONDecoder(object_hook=_object_hook).decode
|
||||
|
||||
|
||||
class Endpoint:
|
||||
__slots__ = ("socket",)
|
||||
|
||||
socket: "ServerConnection"
|
||||
|
||||
def __init__(self, socket):
|
||||
|
||||
10
Options.py
10
Options.py
@@ -1380,7 +1380,7 @@ class NonLocalItems(ItemSet):
|
||||
|
||||
|
||||
class StartInventory(ItemDict):
|
||||
"""Start with these items."""
|
||||
"""Start with the specified amount of these items. Example: "Bomb: 1" """
|
||||
verify_item_name = True
|
||||
display_name = "Start Inventory"
|
||||
rich_text_doc = True
|
||||
@@ -1388,7 +1388,7 @@ class StartInventory(ItemDict):
|
||||
|
||||
|
||||
class StartInventoryPool(StartInventory):
|
||||
"""Start with these items and don't place them in the world.
|
||||
"""Start with the specified amount of these items and don't place them in the world. Example: "Bomb: 1"
|
||||
|
||||
The game decides what the replacement items will be.
|
||||
"""
|
||||
@@ -1474,8 +1474,10 @@ class ItemLinks(OptionList):
|
||||
super(ItemLinks, self).verify(world, player_name, plando_options)
|
||||
existing_links = set()
|
||||
for link in self.value:
|
||||
link["name"] = link["name"].strip()[:16].strip()
|
||||
if link["name"] in existing_links:
|
||||
raise Exception(f"You cannot have more than one link named {link['name']}.")
|
||||
raise Exception(f"Item link names are limited to their first 16 characters and must be unique. "
|
||||
f"You have more than one link named '{link['name']}'.")
|
||||
existing_links.add(link["name"])
|
||||
|
||||
pool = self.verify_items(link["item_pool"], link["name"], "item_pool", world)
|
||||
@@ -1710,7 +1712,7 @@ def generate_yaml_templates(target_folder: typing.Union[str, "pathlib.Path"], ge
|
||||
from jinja2 import Template
|
||||
|
||||
from worlds import AutoWorldRegister
|
||||
from Utils import local_path, __version__, tuplize_version
|
||||
from Utils import local_path, __version__
|
||||
|
||||
full_path: str
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ from json import loads, dumps
|
||||
from CommonClient import CommonContext, server_loop, ClientCommandProcessor, gui_enabled, get_base_parser
|
||||
|
||||
import Utils
|
||||
from settings import Settings
|
||||
import settings
|
||||
from Utils import async_start
|
||||
from MultiServer import mark_raw
|
||||
if typing.TYPE_CHECKING:
|
||||
@@ -286,7 +286,7 @@ class SNESState(enum.IntEnum):
|
||||
|
||||
|
||||
def launch_sni() -> None:
|
||||
sni_path = Settings.sni_options.sni_path
|
||||
sni_path = settings.get_settings().sni_options.sni_path
|
||||
|
||||
if not os.path.isdir(sni_path):
|
||||
sni_path = Utils.local_path(sni_path)
|
||||
@@ -669,7 +669,7 @@ async def game_watcher(ctx: SNIContext) -> None:
|
||||
|
||||
|
||||
async def run_game(romfile: str) -> None:
|
||||
auto_start = Settings.sni_options.snes_rom_start
|
||||
auto_start = settings.get_settings().sni_options.snes_rom_start
|
||||
if auto_start is True:
|
||||
import webbrowser
|
||||
webbrowser.open(romfile)
|
||||
|
||||
14
Utils.py
14
Utils.py
@@ -35,7 +35,7 @@ if typing.TYPE_CHECKING:
|
||||
|
||||
|
||||
def tuplize_version(version: str) -> Version:
|
||||
return Version(*(int(piece, 10) for piece in version.split(".")))
|
||||
return Version(*(int(piece) for piece in version.split(".")))
|
||||
|
||||
|
||||
class Version(typing.NamedTuple):
|
||||
@@ -49,7 +49,6 @@ class Version(typing.NamedTuple):
|
||||
|
||||
__version__ = "0.6.4"
|
||||
version_tuple = tuplize_version(__version__)
|
||||
version = Version(*version_tuple)
|
||||
|
||||
is_linux = sys.platform.startswith("linux")
|
||||
is_macos = sys.platform == "darwin"
|
||||
@@ -721,13 +720,22 @@ def get_intended_text(input_text: str, possible_answers) -> typing.Tuple[str, bo
|
||||
|
||||
|
||||
def get_input_text_from_response(text: str, command: str) -> typing.Optional[str]:
|
||||
"""
|
||||
Parses the response text from `get_intended_text` to find the suggested input and autocomplete the command in
|
||||
arguments with it.
|
||||
|
||||
:param text: The response text from `get_intended_text`.
|
||||
:param command: The command to which the input text should be added. Must contain the prefix used by the command
|
||||
(`!` or `/`).
|
||||
:return: The command with the suggested input text appended, or None if no suggestion was found.
|
||||
"""
|
||||
if "did you mean " in text:
|
||||
for question in ("Didn't find something that closely matches",
|
||||
"Too many close matches"):
|
||||
if text.startswith(question):
|
||||
name = get_text_between(text, "did you mean '",
|
||||
"'? (")
|
||||
return f"!{command} {name}"
|
||||
return f"{command} {name}"
|
||||
elif text.startswith("Missing: "):
|
||||
return text.replace("Missing: ", "!hint_location ")
|
||||
return None
|
||||
|
||||
@@ -109,6 +109,13 @@ if __name__ == "__main__":
|
||||
logging.exception(e)
|
||||
logging.warning("Could not update LttP sprites.")
|
||||
app = get_app()
|
||||
from worlds import AutoWorldRegister
|
||||
# Update to only valid WebHost worlds
|
||||
invalid_worlds = {name for name, world in AutoWorldRegister.world_types.items()
|
||||
if not hasattr(world.web, "tutorials")}
|
||||
if invalid_worlds:
|
||||
logging.error(f"Following worlds not loaded as they are invalid for WebHost: {invalid_worlds}")
|
||||
AutoWorldRegister.world_types = {k: v for k, v in AutoWorldRegister.world_types.items() if k not in invalid_worlds}
|
||||
create_options_files()
|
||||
copy_tutorials_files_to_static()
|
||||
if app.config["SELFLAUNCH"]:
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
import json
|
||||
import typing
|
||||
from uuid import UUID
|
||||
|
||||
from flask import request, session, url_for
|
||||
from markupsafe import Markup
|
||||
from pony.orm import commit, select
|
||||
from pony.orm import commit
|
||||
|
||||
from Utils import restricted_dumps
|
||||
from WebHostLib import app, cache
|
||||
from WebHostLib import app
|
||||
from WebHostLib.check import get_yaml_data, roll_options
|
||||
from WebHostLib.generate import get_meta
|
||||
from WebHostLib.models import Generation, STATE_QUEUED, STATE_STARTED, Seed, STATE_ERROR
|
||||
from WebHostLib.models import Generation, STATE_QUEUED, Seed, STATE_ERROR
|
||||
from . import api_endpoints
|
||||
|
||||
|
||||
@@ -75,23 +74,12 @@ def generate_api():
|
||||
def wait_seed_api(seed: UUID):
|
||||
seed_id = seed
|
||||
seed = Seed.get(id=seed_id)
|
||||
reply_dict: dict[str, typing.Any] = {"queue_len": get_queue_length()}
|
||||
if seed:
|
||||
reply_dict["text"] = "Generation done"
|
||||
return reply_dict, 201
|
||||
return {"text": "Generation done"}, 201
|
||||
generation = Generation.get(id=seed_id)
|
||||
|
||||
if not generation:
|
||||
reply_dict["text"] = "Generation not found"
|
||||
return reply_dict, 404
|
||||
return {"text": "Generation not found"}, 404
|
||||
elif generation.state == STATE_ERROR:
|
||||
reply_dict["text"] = "Generation failed"
|
||||
return reply_dict, 500
|
||||
reply_dict["text"] = "Generation running"
|
||||
return reply_dict, 202
|
||||
|
||||
|
||||
@cache.memoize(timeout=5)
|
||||
def get_queue_length() -> int:
|
||||
return select(generation for generation in Generation if
|
||||
generation.state == STATE_STARTED or generation.state == STATE_QUEUED).count()
|
||||
return {"text": "Generation failed"}, 500
|
||||
return {"text": "Generation running"}, 202
|
||||
|
||||
@@ -19,7 +19,10 @@ from pony.orm import commit, db_session, select
|
||||
|
||||
import Utils
|
||||
|
||||
from MultiServer import Context, server, auto_shutdown, ServerCommandProcessor, ClientMessageProcessor, load_server_cert
|
||||
from MultiServer import (
|
||||
Context, server, auto_shutdown, ServerCommandProcessor, ClientMessageProcessor, load_server_cert,
|
||||
server_per_message_deflate_factory,
|
||||
)
|
||||
from Utils import restricted_loads, cache_argsless
|
||||
from .locker import Locker
|
||||
from .models import Command, GameDataPackage, Room, db
|
||||
@@ -97,6 +100,7 @@ class WebHostContext(Context):
|
||||
self.main_loop.call_soon_threadsafe(cmdprocessor, command.commandtext)
|
||||
command.delete()
|
||||
commit()
|
||||
del commands
|
||||
time.sleep(5)
|
||||
|
||||
@db_session
|
||||
@@ -146,13 +150,13 @@ class WebHostContext(Context):
|
||||
self.location_name_groups = static_location_name_groups
|
||||
return self._load(multidata, game_data_packages, True)
|
||||
|
||||
@db_session
|
||||
def init_save(self, enabled: bool = True):
|
||||
self.saving = enabled
|
||||
if self.saving:
|
||||
savegame_data = Room.get(id=self.room_id).multisave
|
||||
if savegame_data:
|
||||
self.set_save(restricted_loads(Room.get(id=self.room_id).multisave))
|
||||
with db_session:
|
||||
savegame_data = Room.get(id=self.room_id).multisave
|
||||
if savegame_data:
|
||||
self.set_save(restricted_loads(Room.get(id=self.room_id).multisave))
|
||||
self._start_async_saving(atexit_save=False)
|
||||
threading.Thread(target=self.listen_to_db_commands, daemon=True).start()
|
||||
|
||||
@@ -282,8 +286,12 @@ def run_server_process(name: str, ponyconfig: dict, static_server_data: dict,
|
||||
assert ctx.server is None
|
||||
try:
|
||||
ctx.server = websockets.serve(
|
||||
functools.partial(server, ctx=ctx), ctx.host, ctx.port, ssl=get_ssl_context())
|
||||
|
||||
functools.partial(server, ctx=ctx),
|
||||
ctx.host,
|
||||
ctx.port,
|
||||
ssl=get_ssl_context(),
|
||||
extensions=[server_per_message_deflate_factory],
|
||||
)
|
||||
await ctx.server
|
||||
except OSError: # likely port in use
|
||||
ctx.server = websockets.serve(
|
||||
@@ -304,6 +312,7 @@ def run_server_process(name: str, ponyconfig: dict, static_server_data: dict,
|
||||
with db_session:
|
||||
room = Room.get(id=ctx.room_id)
|
||||
room.last_port = port
|
||||
del room
|
||||
else:
|
||||
ctx.logger.exception("Could not determine port. Likely hosting failure.")
|
||||
with db_session:
|
||||
@@ -322,6 +331,7 @@ def run_server_process(name: str, ponyconfig: dict, static_server_data: dict,
|
||||
with db_session:
|
||||
room = Room.get(id=room_id)
|
||||
room.last_port = -1
|
||||
del room
|
||||
logger.exception(e)
|
||||
raise
|
||||
else:
|
||||
@@ -333,11 +343,12 @@ def run_server_process(name: str, ponyconfig: dict, static_server_data: dict,
|
||||
ctx.save_dirty = False # make sure the saving thread does not write to DB after final wakeup
|
||||
ctx.exit_event.set() # make sure the saving thread stops at some point
|
||||
# NOTE: async saving should probably be an async task and could be merged with shutdown_task
|
||||
with (db_session):
|
||||
with db_session:
|
||||
# ensure the Room does not spin up again on its own, minute of safety buffer
|
||||
room = Room.get(id=room_id)
|
||||
room.last_activity = datetime.datetime.utcnow() - \
|
||||
datetime.timedelta(minutes=1, seconds=room.timeout)
|
||||
del room
|
||||
logging.info(f"Shutting down room {room_id} on {name}.")
|
||||
finally:
|
||||
await asyncio.sleep(5)
|
||||
|
||||
@@ -33,6 +33,7 @@ def get_meta(options_source: dict, race: bool = False) -> dict[str, list[str] |
|
||||
"release_mode": str(options_source.get("release_mode", ServerOptions.release_mode)),
|
||||
"remaining_mode": str(options_source.get("remaining_mode", ServerOptions.remaining_mode)),
|
||||
"collect_mode": str(options_source.get("collect_mode", ServerOptions.collect_mode)),
|
||||
"countdown_mode": str(options_source.get("countdown_mode", ServerOptions.countdown_mode)),
|
||||
"item_cheat": bool(int(options_source.get("item_cheat", not ServerOptions.disable_item_cheat))),
|
||||
"server_password": str(options_source.get("server_password", None)),
|
||||
}
|
||||
@@ -72,6 +73,10 @@ def generate(race=False):
|
||||
return render_template("generate.html", race=race, version=__version__)
|
||||
|
||||
|
||||
def format_exception(e: BaseException) -> str:
|
||||
return f"{e.__class__.__name__}: {e}"
|
||||
|
||||
|
||||
def start_generation(options: dict[str, dict | str], meta: dict[str, Any]):
|
||||
results, gen_options = roll_options(options, set(meta["plando_options"]))
|
||||
|
||||
@@ -92,7 +97,9 @@ def start_generation(options: dict[str, dict | str], meta: dict[str, Any]):
|
||||
except PicklingError as e:
|
||||
from .autolauncher import handle_generation_failure
|
||||
handle_generation_failure(e)
|
||||
return render_template("seedError.html", seed_error=("PicklingError: " + str(e)))
|
||||
meta["error"] = format_exception(e)
|
||||
details = json.dumps(meta, indent=4).strip()
|
||||
return render_template("seedError.html", seed_error=meta["error"], details=details)
|
||||
|
||||
commit()
|
||||
|
||||
@@ -104,7 +111,9 @@ def start_generation(options: dict[str, dict | str], meta: dict[str, Any]):
|
||||
except BaseException as e:
|
||||
from .autolauncher import handle_generation_failure
|
||||
handle_generation_failure(e)
|
||||
return render_template("seedError.html", seed_error=(e.__class__.__name__ + ": " + str(e)))
|
||||
meta["error"] = format_exception(e)
|
||||
details = json.dumps(meta, indent=4).strip()
|
||||
return render_template("seedError.html", seed_error=meta["error"], details=details)
|
||||
|
||||
return redirect(url_for("view_seed", seed=seed_id))
|
||||
|
||||
@@ -175,9 +184,9 @@ def gen_game(gen_options: dict, meta: dict[str, Any] | None = None, owner=None,
|
||||
if gen is not None:
|
||||
gen.state = STATE_ERROR
|
||||
meta = json.loads(gen.meta)
|
||||
meta["error"] = (
|
||||
"Allowed time for Generation exceeded, please consider generating locally instead. " +
|
||||
e.__class__.__name__ + ": " + str(e))
|
||||
meta["error"] = ("Allowed time for Generation exceeded, " +
|
||||
"please consider generating locally instead. " +
|
||||
format_exception(e))
|
||||
gen.meta = json.dumps(meta)
|
||||
commit()
|
||||
except BaseException as e:
|
||||
@@ -187,7 +196,7 @@ def gen_game(gen_options: dict, meta: dict[str, Any] | None = None, owner=None,
|
||||
if gen is not None:
|
||||
gen.state = STATE_ERROR
|
||||
meta = json.loads(gen.meta)
|
||||
meta["error"] = (e.__class__.__name__ + ": " + str(e))
|
||||
meta["error"] = format_exception(e)
|
||||
gen.meta = json.dumps(meta)
|
||||
commit()
|
||||
raise
|
||||
@@ -204,7 +213,9 @@ def wait_seed(seed: UUID):
|
||||
if not generation:
|
||||
return "Generation not found."
|
||||
elif generation.state == STATE_ERROR:
|
||||
return render_template("seedError.html", seed_error=generation.meta)
|
||||
meta = json.loads(generation.meta)
|
||||
details = json.dumps(meta, indent=4).strip()
|
||||
return render_template("seedError.html", seed_error=meta["error"], details=details)
|
||||
return render_template("waitSeed.html", seed_id=seed_id)
|
||||
|
||||
|
||||
|
||||
@@ -271,9 +271,9 @@ def host_room(room: UUID):
|
||||
or "Discordbot" in request.user_agent.string
|
||||
or not any(browser_token in request.user_agent.string for browser_token in browser_tokens))
|
||||
|
||||
def get_log(max_size: int = 0 if automated else 1024000) -> str:
|
||||
def get_log(max_size: int = 0 if automated else 1024000) -> Tuple[str, int]:
|
||||
if max_size == 0:
|
||||
return "…"
|
||||
return "…", 0
|
||||
try:
|
||||
with open(os.path.join("logs", str(room.id) + ".txt"), "rb") as log:
|
||||
raw_size = 0
|
||||
@@ -284,9 +284,9 @@ def host_room(room: UUID):
|
||||
break
|
||||
raw_size += len(block)
|
||||
fragments.append(block.decode("utf-8"))
|
||||
return "".join(fragments)
|
||||
return "".join(fragments), raw_size
|
||||
except FileNotFoundError:
|
||||
return ""
|
||||
return "", 0
|
||||
|
||||
return render_template("hostRoom.html", room=room, should_refresh=should_refresh, get_log=get_log)
|
||||
|
||||
|
||||
@@ -231,7 +231,7 @@ def generate_yaml(game: str):
|
||||
if key_parts[-1] == "qty":
|
||||
if key_parts[0] not in options:
|
||||
options[key_parts[0]] = {}
|
||||
if val != "0":
|
||||
if val and val != "0":
|
||||
options[key_parts[0]][key_parts[1]] = int(val)
|
||||
del options[key]
|
||||
|
||||
|
||||
@@ -66,7 +66,7 @@ is to ensure items necessary to complete the game will be accessible to the play
|
||||
rules allowing certain items to be placed in normally unreachable locations, provided the player has indicated they are
|
||||
comfortable exploiting certain glitches in the game.
|
||||
|
||||
## I want to add a game to the Archipelago randomizer. How do I do that?
|
||||
## I want to develop a game implementation for Archipelago. How do I do that?
|
||||
|
||||
The best way to get started is to take a look at our code on GitHub:
|
||||
[Archipelago GitHub Page](https://github.com/ArchipelagoMW/Archipelago).
|
||||
@@ -77,4 +77,5 @@ There, you will find examples of games in the `worlds` folder:
|
||||
You may also find developer documentation in the `docs` folder:
|
||||
[/docs Folder in Archipelago Code](https://github.com/ArchipelagoMW/Archipelago/tree/main/docs).
|
||||
|
||||
If you have more questions, feel free to ask in the **#ap-world-dev** channel on our Discord.
|
||||
If you have more questions regarding development of a game implementation, feel free to ask in the **#ap-world-dev**
|
||||
channel on our Discord.
|
||||
|
||||
@@ -72,3 +72,13 @@ code{
|
||||
padding-right: 0.25rem;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
code.grassy {
|
||||
background-color: #b5e9a4;
|
||||
border: 1px solid #2a6c2f;
|
||||
white-space: preserve;
|
||||
text-align: left;
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
@@ -13,3 +13,7 @@
|
||||
min-height: 360px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
h2, h4 {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
@@ -58,8 +58,7 @@
|
||||
Open Log File...
|
||||
</a>
|
||||
</div>
|
||||
{% set log = get_log() -%}
|
||||
{%- set log_len = log | length - 1 if log.endswith("…") else log | length -%}
|
||||
{% set log, log_len = get_log() -%}
|
||||
<div id="logger" style="white-space: pre">{{ log }}</div>
|
||||
<script>
|
||||
let url = '{{ url_for('display_log', room = room.id) }}';
|
||||
|
||||
@@ -4,16 +4,20 @@
|
||||
|
||||
{% block head %}
|
||||
<title>Generation failed, please retry.</title>
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/waitSeed.css") }}"/>
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='styles/waitSeed.css') }}"/>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
{% include 'header/oceanIslandHeader.html' %}
|
||||
<div id="wait-seed-wrapper" class="grass-island">
|
||||
<div id="wait-seed">
|
||||
<h1>Generation failed</h1>
|
||||
<h2>please retry</h2>
|
||||
{{ seed_error }}
|
||||
<h1>Generation Failed</h1>
|
||||
<h2>Please try again!</h2>
|
||||
<p>{{ seed_error }}</p>
|
||||
<h4>More details:</h4>
|
||||
<p>
|
||||
<code class="grassy">{{ details }}</code>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -31,6 +31,9 @@
|
||||
{% include 'header/oceanHeader.html' %}
|
||||
<div id="games" class="markdown">
|
||||
<h1>Currently Supported Games</h1>
|
||||
<p>Below are the games that are currently included with the Archipelago software. To play a game that is not on
|
||||
this page, please refer to the <a href="/tutorial/Archipelago/setup/en#playing-with-custom-worlds">playing with
|
||||
custom worlds</a> section of the setup guide.</p>
|
||||
<div class="js-only">
|
||||
<label for="game-search">Search for your game below!</label><br />
|
||||
<div class="page-controls">
|
||||
|
||||
@@ -30,21 +30,10 @@
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
if (data.queue_len === 1){
|
||||
waitSeedDiv.innerHTML = `
|
||||
<h1>Generation in Progress</h1>
|
||||
<p>${data.text}</p>
|
||||
<p>This is the only generation in the queue.</p>
|
||||
`;
|
||||
}
|
||||
else {
|
||||
waitSeedDiv.innerHTML = `
|
||||
<h1>Generation in Progress</h1>
|
||||
<p>${data.text}</p>
|
||||
<p>There are ${data.queue_len} generations in the queue.</p>
|
||||
`;
|
||||
}
|
||||
|
||||
waitSeedDiv.innerHTML = `
|
||||
<h1>Generation in Progress</h1>
|
||||
<p>${data.text}</p>
|
||||
`;
|
||||
|
||||
setTimeout(checkStatus, 1000); // Continue polling.
|
||||
} catch (error) {
|
||||
|
||||
@@ -23,18 +23,61 @@ Metadata about the apworld is defined in an `archipelago.json` file inside the z
|
||||
The current format version has at minimum:
|
||||
```json
|
||||
{
|
||||
"version": 6,
|
||||
"compatible_version": 5,
|
||||
"version": 7,
|
||||
"compatible_version": 7,
|
||||
"game": "Game Name"
|
||||
}
|
||||
```
|
||||
|
||||
with the following optional version fields using the format `"1.0.0"` to represent major.minor.build:
|
||||
* `minimum_ap_version` and `maximum_ap_version` - which if present will each be compared against the current
|
||||
Archipelago version respectively to filter those files from being loaded
|
||||
* `world_version` - an arbitrary version for that world in order to only load the newest valid world.
|
||||
An apworld without a world_version is always treated as older than one with a version
|
||||
The `version` and `compatible_version` fields refer to Archipelago's internal file packaging scheme
|
||||
and get automatically added to the `archipelago.json` of an .apworld if it is packaged using the
|
||||
["Build apworlds" launcher component](#build-apworlds-launcher-component),
|
||||
which is the correct way to package your .apworld as a world developer. Do not write these fields yourself.
|
||||
On the other hand, the `game` field should be present in the world folder's manifest file before packaging.
|
||||
|
||||
There are also the following optional fields:
|
||||
* `minimum_ap_version` and `maximum_ap_version` - which if present will each be compared against the current
|
||||
Archipelago version respectively to filter those files from being loaded.
|
||||
* `world_version` - an arbitrary version for that world in order to only load the newest valid world.
|
||||
An apworld without a world_version is always treated as older than one with a version.
|
||||
(**Must** use exactly the format `"major.minor.build"`, e.g. `1.0.0`)
|
||||
* `authors` - a list of authors, to eventually be displayed in various user-facing places such as WebHost and
|
||||
package managers. Should always be a list of strings.
|
||||
|
||||
### "Build apworlds" Launcher Component
|
||||
|
||||
In the Archipelago Launcher, there is a "Build apworlds" component that will package all world folders to `.apworld`,
|
||||
and add `archipelago.json` manifest files to them.
|
||||
These .apworld files will be output to `build/apworlds` (relative to the Archipelago root directory).
|
||||
The `archipelago.json` file in each .apworld will automatically include the appropriate
|
||||
`version` and `compatible_version`.
|
||||
|
||||
If a world folder has an `archipelago.json` in its root, any fields it contains will be carried over.
|
||||
So, a world folder with an `archipelago.json` that looks like this:
|
||||
|
||||
```json
|
||||
{
|
||||
"game": "Game Name",
|
||||
"minimum_ap_version": "0.6.4",
|
||||
"world_version": "2.1.4",
|
||||
"authors": ["NewSoupVi"]
|
||||
}
|
||||
```
|
||||
|
||||
will be packaged into an `.apworld` with a manifest file inside of it that looks like this:
|
||||
|
||||
```json
|
||||
{
|
||||
"minimum_ap_version": "0.6.4",
|
||||
"world_version": "2.1.4",
|
||||
"authors": ["NewSoupVi"],
|
||||
"version": 7,
|
||||
"compatible_version": 7,
|
||||
"game": "Game Name"
|
||||
}
|
||||
```
|
||||
|
||||
This is the recommended workflow for packaging your world to an `.apworld`.
|
||||
|
||||
## Extra Data
|
||||
|
||||
|
||||
@@ -180,8 +180,8 @@ Root: HKCR; Subkey: "{#MyAppName}mm2patch\shell\open\command"; ValueData: """{a
|
||||
|
||||
Root: HKCR; Subkey: ".apladx"; ValueData: "{#MyAppName}ladxpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}ladxpatch"; ValueData: "Archipelago Links Awakening DX Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}ladxpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoLinksAwakeningClient.exe,0"; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}ladxpatch\shell\open\command"; ValueData: """{app}\ArchipelagoLinksAwakeningClient.exe"" ""%1"""; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}ladxpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoLauncher.exe,0"; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}ladxpatch\shell\open\command"; ValueData: """{app}\ArchipelagoLauncher.exe"" ""%1"""; ValueType: string; ValueName: "";
|
||||
|
||||
Root: HKCR; Subkey: ".aptloz"; ValueData: "{#MyAppName}tlozpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}tlozpatch"; ValueData: "Archipelago The Legend of Zelda Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
|
||||
|
||||
10
kvui.py
10
kvui.py
@@ -838,15 +838,15 @@ class GameManager(ThemedApp):
|
||||
self.log_panels: typing.Dict[str, Widget] = {}
|
||||
|
||||
# keep track of last used command to autofill on click
|
||||
self.last_autofillable_command = "hint"
|
||||
autofillable_commands = ("hint_location", "hint", "getitem")
|
||||
self.last_autofillable_command = "!hint"
|
||||
autofillable_commands = ("!hint_location", "!hint", "!getitem")
|
||||
original_say = ctx.on_user_say
|
||||
|
||||
def intercept_say(text):
|
||||
text = original_say(text)
|
||||
if text:
|
||||
for command in autofillable_commands:
|
||||
if text.startswith("!" + command):
|
||||
if text.startswith(command):
|
||||
self.last_autofillable_command = command
|
||||
break
|
||||
return text
|
||||
@@ -1099,10 +1099,6 @@ class GameManager(ThemedApp):
|
||||
hints = self.ctx.stored_data.get(f"_read_hints_{self.ctx.team}_{self.ctx.slot}", [])
|
||||
self.hint_log.refresh_hints(hints)
|
||||
|
||||
# default F1 keybind, opens a settings menu, that seems to break the layout engine once closed
|
||||
def open_settings(self, *largs):
|
||||
pass
|
||||
|
||||
|
||||
class LogtoUI(logging.Handler):
|
||||
def __init__(self, on_log):
|
||||
|
||||
12
settings.py
12
settings.py
@@ -579,6 +579,17 @@ class ServerOptions(Group):
|
||||
"goal" -> Client can ask for remaining items after goal completion
|
||||
"""
|
||||
|
||||
class CountdownMode(str):
|
||||
"""
|
||||
Countdown modes
|
||||
Determines whether or not a player can initiate a countdown with !countdown
|
||||
Note that /countdown is always available to the host.
|
||||
|
||||
"enabled" -> Client can always initiate a countdown with !countdown.
|
||||
"disabled" -> Client can never initiate a countdown with !countdown.
|
||||
"auto" -> !countdown will be available for any room with less than 30 slots.
|
||||
"""
|
||||
|
||||
class AutoShutdown(int):
|
||||
"""Automatically shut down the server after this many seconds without new location checks, 0 to keep running"""
|
||||
|
||||
@@ -613,6 +624,7 @@ class ServerOptions(Group):
|
||||
release_mode: ReleaseMode = ReleaseMode("auto")
|
||||
collect_mode: CollectMode = CollectMode("auto")
|
||||
remaining_mode: RemainingMode = RemainingMode("goal")
|
||||
countdown_mode: CountdownMode = CountdownMode("auto")
|
||||
auto_shutdown: AutoShutdown = AutoShutdown(0)
|
||||
compatibility: Compatibility = Compatibility(2)
|
||||
log_network: LogNetwork = LogNetwork(0)
|
||||
|
||||
14
setup.py
14
setup.py
@@ -372,7 +372,6 @@ class BuildExeCommand(cx_Freeze.command.build_exe.build_exe):
|
||||
from Options import generate_yaml_templates
|
||||
from worlds.AutoWorld import AutoWorldRegister
|
||||
from worlds.Files import APWorldContainer
|
||||
from Utils import version
|
||||
assert not non_apworlds - set(AutoWorldRegister.world_types), \
|
||||
f"Unknown world {non_apworlds - set(AutoWorldRegister.world_types)} designated for .apworld"
|
||||
folders_to_remove: list[str] = []
|
||||
@@ -383,14 +382,23 @@ class BuildExeCommand(cx_Freeze.command.build_exe.build_exe):
|
||||
world_directory = self.libfolder / "worlds" / file_name
|
||||
if os.path.isfile(world_directory / "archipelago.json"):
|
||||
manifest = json.load(open(world_directory / "archipelago.json"))
|
||||
|
||||
assert "game" in manifest, (
|
||||
f"World directory {world_directory} has an archipelago.json manifest file, but it"
|
||||
"does not define a \"game\"."
|
||||
)
|
||||
assert manifest["game"] == worldtype.game, (
|
||||
f"World directory {world_directory} has an archipelago.json manifest file, but value of the"
|
||||
f"\"game\" field ({manifest['game']} does not equal the World class's game ({worldtype.game})."
|
||||
)
|
||||
else:
|
||||
manifest = {}
|
||||
# this method creates an apworld that cannot be moved to a different OS or minor python version,
|
||||
# which should be ok
|
||||
zip_path = self.libfolder / "worlds" / (file_name + ".apworld")
|
||||
apworld = APWorldContainer(str(zip_path))
|
||||
apworld.minimum_ap_version = version
|
||||
apworld.maximum_ap_version = version
|
||||
apworld.minimum_ap_version = version_tuple
|
||||
apworld.maximum_ap_version = version_tuple
|
||||
apworld.game = worldtype.game
|
||||
manifest.update(apworld.get_manifest())
|
||||
apworld.manifest_path = f"{file_name}/archipelago.json"
|
||||
|
||||
0
test/benchmark/compression/__init__.py
Normal file
0
test/benchmark/compression/__init__.py
Normal file
227
test/benchmark/compression/benchmark.py
Normal file
227
test/benchmark/compression/benchmark.py
Normal file
@@ -0,0 +1,227 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
# based on python-websockets compression benchmark (c) Aymeric Augustin and contributors
|
||||
# https://github.com/python-websockets/websockets/blob/main/experiments/compression/benchmark.py
|
||||
|
||||
import collections
|
||||
import time
|
||||
import zlib
|
||||
from typing import Iterable
|
||||
|
||||
|
||||
REPEAT = 10
|
||||
|
||||
WB, ML = 12, 5 # defaults used as a reference
|
||||
WBITS = range(9, 16)
|
||||
MEMLEVELS = range(1, 10)
|
||||
|
||||
|
||||
def benchmark(data: Iterable[bytes]) -> None:
|
||||
size: dict[int, dict[int, float]] = collections.defaultdict(dict)
|
||||
duration: dict[int, dict[int, float]] = collections.defaultdict(dict)
|
||||
|
||||
for wbits in WBITS:
|
||||
for memLevel in MEMLEVELS:
|
||||
encoder = zlib.compressobj(wbits=-wbits, memLevel=memLevel)
|
||||
encoded = []
|
||||
|
||||
print(f"Compressing {REPEAT} times with {wbits=} and {memLevel=}")
|
||||
|
||||
t0 = time.perf_counter()
|
||||
|
||||
for _ in range(REPEAT):
|
||||
for item in data:
|
||||
# Taken from PerMessageDeflate.encode
|
||||
item = encoder.compress(item) + encoder.flush(zlib.Z_SYNC_FLUSH)
|
||||
if item.endswith(b"\x00\x00\xff\xff"):
|
||||
item = item[:-4]
|
||||
encoded.append(item)
|
||||
|
||||
t1 = time.perf_counter()
|
||||
|
||||
size[wbits][memLevel] = sum(len(item) for item in encoded) / REPEAT
|
||||
duration[wbits][memLevel] = (t1 - t0) / REPEAT
|
||||
|
||||
raw_size = sum(len(item) for item in data)
|
||||
|
||||
print("=" * 79)
|
||||
print("Compression ratio")
|
||||
print("=" * 79)
|
||||
print("\t".join(["wb \\ ml"] + [str(memLevel) for memLevel in MEMLEVELS]))
|
||||
for wbits in WBITS:
|
||||
print(
|
||||
"\t".join(
|
||||
[str(wbits)]
|
||||
+ [
|
||||
f"{100 * (1 - size[wbits][memLevel] / raw_size):.1f}%"
|
||||
for memLevel in MEMLEVELS
|
||||
]
|
||||
)
|
||||
)
|
||||
print("=" * 79)
|
||||
print()
|
||||
|
||||
print("=" * 79)
|
||||
print("CPU time")
|
||||
print("=" * 79)
|
||||
print("\t".join(["wb \\ ml"] + [str(memLevel) for memLevel in MEMLEVELS]))
|
||||
for wbits in WBITS:
|
||||
print(
|
||||
"\t".join(
|
||||
[str(wbits)]
|
||||
+ [
|
||||
f"{1000 * duration[wbits][memLevel]:.1f}ms"
|
||||
for memLevel in MEMLEVELS
|
||||
]
|
||||
)
|
||||
)
|
||||
print("=" * 79)
|
||||
print()
|
||||
|
||||
print("=" * 79)
|
||||
print(f"Size vs. {WB} \\ {ML}")
|
||||
print("=" * 79)
|
||||
print("\t".join(["wb \\ ml"] + [str(memLevel) for memLevel in MEMLEVELS]))
|
||||
for wbits in WBITS:
|
||||
print(
|
||||
"\t".join(
|
||||
[str(wbits)]
|
||||
+ [
|
||||
f"{100 * (size[wbits][memLevel] / size[WB][ML] - 1):.1f}%"
|
||||
for memLevel in MEMLEVELS
|
||||
]
|
||||
)
|
||||
)
|
||||
print("=" * 79)
|
||||
print()
|
||||
|
||||
print("=" * 79)
|
||||
print(f"Time vs. {WB} \\ {ML}")
|
||||
print("=" * 79)
|
||||
print("\t".join(["wb \\ ml"] + [str(memLevel) for memLevel in MEMLEVELS]))
|
||||
for wbits in WBITS:
|
||||
print(
|
||||
"\t".join(
|
||||
[str(wbits)]
|
||||
+ [
|
||||
f"{100 * (duration[wbits][memLevel] / duration[WB][ML] - 1):.1f}%"
|
||||
for memLevel in MEMLEVELS
|
||||
]
|
||||
)
|
||||
)
|
||||
print("=" * 79)
|
||||
print()
|
||||
|
||||
|
||||
def generate_data_package_corpus() -> list[bytes]:
|
||||
# compared to default 12, 5:
|
||||
# 11, 4 saves 16K RAM, gives +4.6% size, -5.0% time .. +1.1% time
|
||||
# 10, 4 saves 20K RAM, gives +10.2% size, -3.8% time .. +0.6% time
|
||||
# 11, 3 saves 20K RAM, gives +6.5% size, +14.2% time
|
||||
# 10, 3 saves 24K RAM, gives +12.8% size, +0.5% time .. +6.9% time
|
||||
# NOTE: time delta is highly unstable; time is ~100ms
|
||||
import warnings
|
||||
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore")
|
||||
|
||||
from NetUtils import encode
|
||||
from worlds import network_data_package
|
||||
|
||||
return [encode(network_data_package).encode("utf-8")]
|
||||
|
||||
|
||||
def generate_solo_release_corpus() -> list[bytes]:
|
||||
# compared to default 12, 5:
|
||||
# 11, 4 saves 16K RAM, gives +0.9% size, +3.9% time
|
||||
# 10, 4 saves 20K RAM, gives +1.4% size, +3.4% time
|
||||
# 11, 3 saves 20K RAM, gives +1.8% size, +13.9% time
|
||||
# 10, 3 saves 24K RAM, gives +2.1% size, +4.8% time
|
||||
# NOTE: time delta is highly unstable; time is ~0.4ms
|
||||
|
||||
from random import Random
|
||||
from MultiServer import json_format_send_event
|
||||
from NetUtils import encode, NetworkItem
|
||||
|
||||
r = Random()
|
||||
r.seed(0)
|
||||
solo_release = []
|
||||
solo_release_locations = [r.randint(1000, 1999) for _ in range(200)]
|
||||
solo_release_items = sorted([r.randint(1000, 1999) for _ in range(200)]) # currently sorted by item
|
||||
solo_player = 1
|
||||
for location, item in zip(solo_release_locations, solo_release_items):
|
||||
flags = r.choice((0, 0, 0, 0, 0, 0, 0, 1, 2, 3))
|
||||
network_item = NetworkItem(item, location, solo_player, flags)
|
||||
solo_release.append(json_format_send_event(network_item, solo_player))
|
||||
solo_release.append({
|
||||
"cmd": "ReceivedItems",
|
||||
"index": 0,
|
||||
"items": solo_release_items,
|
||||
})
|
||||
solo_release.append({
|
||||
"cmd": "RoomUpdate",
|
||||
"hint_points": 200,
|
||||
"checked_locations": solo_release_locations,
|
||||
})
|
||||
return [encode(solo_release).encode("utf-8")]
|
||||
|
||||
|
||||
def generate_gameplay_corpus() -> list[bytes]:
|
||||
# compared to default 12, 5:
|
||||
# 11, 4 saves 16K RAM, gives +13.6% size, +4.1% time
|
||||
# 10, 4 saves 20K RAM, gives +22.3% size, +2.2% time
|
||||
# 10, 3 saves 24K RAM, gives +26.2% size, +1.6% time
|
||||
# NOTE: time delta is highly unstable; time is 4ms
|
||||
|
||||
from copy import copy
|
||||
from random import Random
|
||||
from MultiServer import json_format_send_event
|
||||
from NetUtils import encode, NetworkItem
|
||||
|
||||
r = Random()
|
||||
r.seed(0)
|
||||
gameplay = []
|
||||
observer = 1
|
||||
hint_points = 0
|
||||
index = 0
|
||||
players = list(range(1, 10))
|
||||
player_locations = {player: [r.randint(1000, 1999) for _ in range(200)] for player in players}
|
||||
player_items = {player: [r.randint(1000, 1999) for _ in range(200)] for player in players}
|
||||
player_receiver = {player: [r.randint(1, len(players)) for _ in range(200)] for player in players}
|
||||
for i in range(0, len(player_locations[1])):
|
||||
player_sequence = copy(players)
|
||||
r.shuffle(player_sequence)
|
||||
for finder in player_sequence:
|
||||
flags = r.choice((0, 0, 0, 0, 0, 0, 0, 1, 2, 3))
|
||||
receiver = player_receiver[finder][i]
|
||||
item = player_items[finder][i]
|
||||
location = player_locations[finder][i]
|
||||
network_item = NetworkItem(item, location, receiver, flags)
|
||||
gameplay.append(json_format_send_event(network_item, observer))
|
||||
if finder == observer:
|
||||
hint_points += 1
|
||||
gameplay.append({
|
||||
"cmd": "RoomUpdate",
|
||||
"hint_points": hint_points,
|
||||
"checked_locations": [location],
|
||||
})
|
||||
if receiver == observer:
|
||||
gameplay.append({
|
||||
"cmd": "ReceivedItems",
|
||||
"index": index,
|
||||
"items": [item],
|
||||
})
|
||||
index += 1
|
||||
return [encode(gameplay).encode("utf-8")]
|
||||
|
||||
|
||||
def main() -> None:
|
||||
#corpus = generate_data_package_corpus()
|
||||
#corpus = generate_solo_release_corpus()
|
||||
#corpus = generate_gameplay_corpus()
|
||||
corpus = generate_data_package_corpus() + generate_solo_release_corpus() + generate_gameplay_corpus()
|
||||
benchmark(corpus)
|
||||
print(f"raw size: {sum(len(data) for data in corpus)}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,4 +1,12 @@
|
||||
def run_locations_benchmark():
|
||||
def run_locations_benchmark(freeze_gc: bool = True) -> None:
|
||||
"""
|
||||
Run a benchmark of location access rule performance against an empty_state and an all_state.
|
||||
|
||||
:param freeze_gc: Whether to freeze gc before benchmarking and unfreeze gc afterward. Freezing gc moves all objects
|
||||
tracked by the garbage collector to a permanent generation, ignoring them in all future collections. Freezing
|
||||
greatly reduces the duration of running gc.collect() within benchmarks, which otherwise often takes much longer
|
||||
than running all iterations for the location rule being benchmarked.
|
||||
"""
|
||||
import argparse
|
||||
import logging
|
||||
import gc
|
||||
@@ -34,6 +42,8 @@ def run_locations_benchmark():
|
||||
return "\n".join(f" {time:.4f} in {name}" for name, time in counter.most_common(top))
|
||||
|
||||
def location_test(self, test_location: Location, state: CollectionState, state_name: str) -> float:
|
||||
if freeze_gc:
|
||||
gc.freeze()
|
||||
with TimeIt(f"{test_location.game} {self.rule_iterations} "
|
||||
f"runs of {test_location}.access_rule({state_name})", logger) as t:
|
||||
for _ in range(self.rule_iterations):
|
||||
@@ -41,6 +51,8 @@ def run_locations_benchmark():
|
||||
# if time is taken to disentangle complex ref chains,
|
||||
# this time should be attributed to the rule.
|
||||
gc.collect()
|
||||
if freeze_gc:
|
||||
gc.unfreeze()
|
||||
return t.dif
|
||||
|
||||
def main(self):
|
||||
@@ -64,9 +76,13 @@ def run_locations_benchmark():
|
||||
|
||||
gc.collect()
|
||||
for step in self.gen_steps:
|
||||
if freeze_gc:
|
||||
gc.freeze()
|
||||
with TimeIt(f"{game} step {step}", logger):
|
||||
call_all(multiworld, step)
|
||||
gc.collect()
|
||||
if freeze_gc:
|
||||
gc.unfreeze()
|
||||
|
||||
locations = sorted(multiworld.get_unfilled_locations())
|
||||
if not locations:
|
||||
|
||||
@@ -6,9 +6,9 @@ from Utils import get_intended_text, get_input_text_from_response
|
||||
class TestClient(unittest.TestCase):
|
||||
def test_autofill_hint_from_fuzzy_hint(self) -> None:
|
||||
tests = (
|
||||
("item", ["item1", "item2"]), # Multiple close matches
|
||||
("itm", ["item1", "item21"]), # No close match, multiple option
|
||||
("item", ["item1"]), # No close match, single option
|
||||
("item", ["item1", "item2"]), # Multiple close matches
|
||||
("itm", ["item1", "item21"]), # No close match, multiple option
|
||||
("item", ["item1"]), # No close match, single option
|
||||
("item", ["\"item\" 'item' (item)"]), # Testing different special characters
|
||||
)
|
||||
|
||||
@@ -16,7 +16,7 @@ class TestClient(unittest.TestCase):
|
||||
item_name, usable, response = get_intended_text(input_text, possible_answers)
|
||||
self.assertFalse(usable, "This test must be updated, it seems get_fuzzy_results behavior changed")
|
||||
|
||||
hint_command = get_input_text_from_response(response, "hint")
|
||||
hint_command = get_input_text_from_response(response, "!hint")
|
||||
self.assertIsNotNone(hint_command,
|
||||
"The response to fuzzy hints is no longer recognized by the hint autofill")
|
||||
self.assertEqual(hint_command, f"!hint {item_name}",
|
||||
|
||||
@@ -175,12 +175,12 @@ class APWorldContainer(APContainer):
|
||||
maximum_ap_version: "Version | None" = None
|
||||
|
||||
def read_contents(self, opened_zipfile: zipfile.ZipFile) -> Dict[str, Any]:
|
||||
from Utils import tuplize_version, Version
|
||||
from Utils import tuplize_version
|
||||
manifest = super().read_contents(opened_zipfile)
|
||||
self.game = manifest["game"]
|
||||
for version_key in ("world_version", "minimum_ap_version", "maximum_ap_version"):
|
||||
if version_key in manifest:
|
||||
setattr(self, version_key, Version(*tuplize_version(manifest[version_key])))
|
||||
setattr(self, version_key, tuplize_version(manifest[version_key]))
|
||||
return manifest
|
||||
|
||||
def get_manifest(self) -> Dict[str, Any]:
|
||||
|
||||
@@ -180,7 +180,7 @@ def _install_apworld(apworld_src: str = "") -> Optional[Tuple[pathlib.Path, path
|
||||
if found_already_loaded and is_kivy_running():
|
||||
raise Exception(f"Installed APWorld successfully, but '{module_name}' is already loaded, "
|
||||
"so a Launcher restart is required to use the new installation.")
|
||||
world_source = worlds.WorldSource(str(target), is_zip=True)
|
||||
world_source = worlds.WorldSource(str(target), is_zip=True, relative=False)
|
||||
bisect.insort(worlds.world_sources, world_source)
|
||||
world_source.load()
|
||||
|
||||
@@ -217,8 +217,6 @@ components: List[Component] = [
|
||||
description="Install an APWorld to play games not included with Archipelago by default."),
|
||||
Component('Text Client', 'CommonClient', 'ArchipelagoTextClient', func=launch_textclient,
|
||||
description="Connect to a multiworld using the text client."),
|
||||
Component('Links Awakening DX Client', 'LinksAwakeningClient',
|
||||
file_identifier=SuffixIdentifier('.apladx')),
|
||||
Component('LttP Adjuster', 'LttPAdjuster'),
|
||||
# Ocarina of Time
|
||||
Component('OoT Client', 'OoTClient',
|
||||
@@ -244,21 +242,45 @@ icon_paths = {
|
||||
}
|
||||
|
||||
if not is_frozen():
|
||||
def _build_apworlds():
|
||||
def _build_apworlds(*launch_args: str):
|
||||
import json
|
||||
import os
|
||||
import zipfile
|
||||
|
||||
from worlds import AutoWorldRegister
|
||||
from worlds.Files import APWorldContainer
|
||||
from Launcher import open_folder
|
||||
|
||||
import argparse
|
||||
parser = argparse.ArgumentParser("Build script for APWorlds")
|
||||
parser.add_argument("worlds", type=str, default=(), nargs="*", help="Names of APWorlds to build.")
|
||||
args = parser.parse_args(launch_args)
|
||||
|
||||
if args.worlds:
|
||||
games = [(game, AutoWorldRegister.world_types.get(game, None)) for game in args.worlds]
|
||||
else:
|
||||
games = [(worldname, worldtype) for worldname, worldtype in AutoWorldRegister.world_types.items()
|
||||
if not worldtype.zip_path]
|
||||
|
||||
apworlds_folder = os.path.join("build", "apworlds")
|
||||
os.makedirs(apworlds_folder, exist_ok=True)
|
||||
for worldname, worldtype in AutoWorldRegister.world_types.items():
|
||||
for worldname, worldtype in games:
|
||||
if not worldtype:
|
||||
logging.error(f"Requested APWorld \"{worldname}\" does not exist.")
|
||||
continue
|
||||
file_name = os.path.split(os.path.dirname(worldtype.__file__))[1]
|
||||
world_directory = os.path.join("worlds", file_name)
|
||||
if os.path.isfile(os.path.join(world_directory, "archipelago.json")):
|
||||
manifest = json.load(open(os.path.join(world_directory, "archipelago.json")))
|
||||
|
||||
assert "game" in manifest, (
|
||||
f"World directory {world_directory} has an archipelago.json manifest file, but it"
|
||||
"does not define a \"game\"."
|
||||
)
|
||||
assert manifest["game"] == worldtype.game, (
|
||||
f"World directory {world_directory} has an archipelago.json manifest file, but value of the"
|
||||
f"\"game\" field ({manifest['game']} does not equal the World class's game ({worldtype.game})."
|
||||
)
|
||||
else:
|
||||
manifest = {}
|
||||
|
||||
@@ -269,12 +291,15 @@ if not is_frozen():
|
||||
apworld.manifest_path = f"{file_name}/archipelago.json"
|
||||
with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED,
|
||||
compresslevel=9) as zf:
|
||||
for path in pathlib.Path(world_directory).rglob("*.*"):
|
||||
for path in pathlib.Path(world_directory).rglob("*"):
|
||||
relative_path = os.path.join(*path.parts[path.parts.index("worlds") + 1:])
|
||||
if "__MACOSX" in relative_path or ".DS_STORE" in relative_path or "__pycache__" in relative_path:
|
||||
continue
|
||||
if not relative_path.endswith("archipelago.json"):
|
||||
zf.write(path, relative_path)
|
||||
zf.writestr(apworld.manifest_path, json.dumps(manifest))
|
||||
open_folder(apworlds_folder)
|
||||
|
||||
components.append(Component('Build apworlds', func=_build_apworlds, cli=True,))
|
||||
|
||||
components.append(Component('Build APWorlds', func=_build_apworlds, cli=True,
|
||||
description="Build APWorlds from loose-file world folders."))
|
||||
|
||||
@@ -128,8 +128,7 @@ for world_source in world_sources:
|
||||
break
|
||||
game = manifest.get("game")
|
||||
if game in AutoWorldRegister.world_types:
|
||||
AutoWorldRegister.world_types[game].world_version = Version(*tuplize_version(manifest.get("world_version",
|
||||
"0.0.0")))
|
||||
AutoWorldRegister.world_types[game].world_version = tuplize_version(manifest.get("world_version", "0.0.0"))
|
||||
|
||||
if apworlds:
|
||||
# encapsulation for namespace / gc purposes
|
||||
|
||||
@@ -23,7 +23,7 @@ game you play will make sure that every game has its own save game.
|
||||
Unzip the Aquaria randomizer release and copy all unzipped files in the Aquaria game folder. The unzipped files are:
|
||||
- aquaria_randomizer.exe
|
||||
- OpenAL32.dll
|
||||
- override (directory)
|
||||
- randomizer_files (directory)
|
||||
- SDL2.dll
|
||||
- usersettings.xml
|
||||
- wrap_oal.dll
|
||||
@@ -32,7 +32,10 @@ Unzip the Aquaria randomizer release and copy all unzipped files in the Aquaria
|
||||
If there is a conflict between files in the original game folder and the unzipped files, you should overwrite
|
||||
the original files with the ones from the unzipped randomizer.
|
||||
|
||||
Finally, to launch the randomizer, you must use the command line interface (you can open the command line interface
|
||||
There is multiple way to start the game. The easiest one is using the launcher. To do that, just run
|
||||
the `aquaria_randomizer.exe` file.
|
||||
|
||||
You can also launch the randomizer using the command line interface (you can open the command line interface
|
||||
by typing `cmd` in the address bar of the Windows File Explorer). Here is the command line used to start the
|
||||
randomizer:
|
||||
|
||||
@@ -49,15 +52,17 @@ aquaria_randomizer.exe --name YourName --server theServer:thePort --password th
|
||||
### Linux when using the AppImage
|
||||
|
||||
If you use the AppImage, just copy it into the Aquaria game folder. You then have to make it executable. You
|
||||
can do that from command line by using:
|
||||
can do that from the command line by using:
|
||||
|
||||
```bash
|
||||
chmod +x Aquaria_Randomizer-*.AppImage
|
||||
```
|
||||
|
||||
or by using the Graphical Explorer of your system.
|
||||
or by using the Graphical file Explorer of your system (the permission can generally be set in the file properties).
|
||||
|
||||
To launch the randomizer, just launch in command line:
|
||||
To launch the randomizer using the integrated launcher, just execute the AppImage file.
|
||||
|
||||
You can also use command line arguments to set the server and slot of your game:
|
||||
|
||||
```bash
|
||||
./Aquaria_Randomizer-*.AppImage --name YourName --server theServer:thePort
|
||||
@@ -79,7 +84,7 @@ the original game will stop working. Copying the folder will guarantee that the
|
||||
|
||||
Untar the Aquaria randomizer release and copy all extracted files in the Aquaria game folder. The extracted files are:
|
||||
- aquaria_randomizer
|
||||
- override (directory)
|
||||
- randomizer_files (directory)
|
||||
- usersettings.xml
|
||||
- cacert.pem
|
||||
|
||||
@@ -87,7 +92,7 @@ If there is a conflict between files in the original game folder and the extract
|
||||
the original files with the ones from the extracted randomizer files.
|
||||
|
||||
Then, you should use your system package manager to install `liblua5`, `libogg`, `libvorbis`, `libopenal` and `libsdl2`.
|
||||
On Debian base system (like Ubuntu), you can use the following command:
|
||||
On Debian base systems (like Ubuntu), you can use the following command:
|
||||
|
||||
```bash
|
||||
sudo apt install liblua5.1-0-dev libogg-dev libvorbis-dev libopenal-dev libsdl2-dev
|
||||
@@ -97,7 +102,9 @@ Also, if there are certain `.so` files in the original Aquaria game folder (`lib
|
||||
`libSDL-1.2.so.0` and `libstdc++.so.6`), you should remove them from the Aquaria Randomizer game folder. Those are
|
||||
old libraries that will not work on the recent build of the randomizer.
|
||||
|
||||
To launch the randomizer, just launch in command line:
|
||||
To launch the randomizer using the integrated launcher, just execute the `aquaria_randomizer` file.
|
||||
|
||||
You can also use command line arguments to set the server and slot of your game:
|
||||
|
||||
```bash
|
||||
./aquaria_randomizer --name YourName --server theServer:thePort
|
||||
@@ -115,6 +122,20 @@ sure that your executable has executable permission:
|
||||
```bash
|
||||
chmod +x aquaria_randomizer
|
||||
```
|
||||
### Steam deck
|
||||
|
||||
On the Steamdeck, go in desktop mode and follow the same procedure as the Linux Appimage.
|
||||
|
||||
|
||||
### No sound on Linux/Steam deck
|
||||
|
||||
If your game play without problems, but with no sound, the game probably does not use the correct
|
||||
driver for the sound system. To fix that, you can use `ALSOFT_DRIVERS=pulse` before your command
|
||||
line to make it work. Something like this (depending on the way you launch the randomizer):
|
||||
|
||||
```bash
|
||||
ALSOFT_DRIVERS=pulse ./Aquaria_Randomizer-*.AppImage --name YourName --server theServer:thePort
|
||||
```
|
||||
|
||||
## Auto-Tracking
|
||||
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
|
||||
## Logiciels nécessaires
|
||||
|
||||
- Une copie du jeu Aquaria non-modifiée (disponible sur la majorité des sites de ventes de jeux vidéos en ligne)
|
||||
- Une copie du jeu Aquaria non modifiée (disponible sur la majorité des sites de ventes de jeux vidéos en ligne)
|
||||
- Le client du Randomizer d'Aquaria [Aquaria randomizer](https://github.com/tioui/Aquaria_Randomizer/releases/latest)
|
||||
|
||||
## Logiciels optionnels
|
||||
|
||||
- De manière optionnel, pour pouvoir envoyer des [commandes](/tutorial/Archipelago/commands/en) comme `!hint`: utilisez le client texte de [la version la plus récente d'Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases/latest)
|
||||
- De manière optionnelle, pour pouvoir envoyer des [commandes](/tutorial/Archipelago/commands/en) comme `!hint`: utilisez le client texte de [la version la plus récente d'Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases/latest)
|
||||
- [Aquaria AP Tracker](https://github.com/palex00/aquaria-ap-tracker/releases/latest), pour utiliser avec [PopTracker](https://github.com/black-sliver/PopTracker/releases/latest)
|
||||
|
||||
## Procédures d'installation et d'exécution
|
||||
@@ -25,7 +25,7 @@ Désarchiver le randomizer d'Aquaria et copier tous les fichiers de l'archive da
|
||||
fichier d'archive devrait contenir les fichiers suivants:
|
||||
- aquaria_randomizer.exe
|
||||
- OpenAL32.dll
|
||||
- override (directory)
|
||||
- randomizer_files (directory)
|
||||
- SDL2.dll
|
||||
- usersettings.xml
|
||||
- wrap_oal.dll
|
||||
@@ -34,7 +34,10 @@ fichier d'archive devrait contenir les fichiers suivants:
|
||||
S'il y a des conflits entre les fichiers de l'archive zip et les fichiers du jeu original, vous devez utiliser
|
||||
les fichiers contenus dans l'archive zip.
|
||||
|
||||
Finalement, pour lancer le randomizer, vous devez utiliser la ligne de commande (vous pouvez ouvrir une interface de
|
||||
Il y a plusieurs manières de lancer le randomizer. Le plus simple consiste à utiliser le lanceur intégré en
|
||||
exécutant simplement le fichier `aquaria_randomizer.exe`.
|
||||
|
||||
Il est également possible de lancer le randomizer en utilisant la ligne de commande (vous pouvez ouvrir une interface de
|
||||
ligne de commande, entrez l'adresse `cmd` dans la barre d'adresse de l'explorateur de fichier de Windows). Voici
|
||||
la ligne de commande à utiliser pour lancer le randomizer:
|
||||
|
||||
@@ -57,9 +60,12 @@ le mettre exécutable. Vous pouvez mettre le fichier exécutable avec la command
|
||||
chmod +x Aquaria_Randomizer-*.AppImage
|
||||
```
|
||||
|
||||
ou bien en utilisant l'explorateur graphique de votre système.
|
||||
ou bien en utilisant l'explorateur de fichier graphique de votre système (la permission d'exécution est
|
||||
généralement dans les propriétés du fichier).
|
||||
|
||||
Pour lancer le randomizer, utiliser la commande suivante:
|
||||
Pour lancer le randomizer en utilisant le lanceur intégré, seulement exécuter le fichier AppImage.
|
||||
|
||||
Vous pouvez également lancer le randomizer en spécifiant les informations de connexion dans les arguments de la ligne de commande:
|
||||
|
||||
```bash
|
||||
./Aquaria_Randomizer-*.AppImage --name VotreNom --server LeServeur:LePort
|
||||
@@ -83,7 +89,7 @@ avant de déposer le randomizer à l'intérieur permet de vous assurer de garder
|
||||
Désarchiver le fichier tar et copier tous les fichiers qu'il contient dans le répertoire du jeu d'origine d'Aquaria. Les
|
||||
fichiers extraient du fichier tar devraient être les suivants:
|
||||
- aquaria_randomizer
|
||||
- override (directory)
|
||||
- randomizer_files (directory)
|
||||
- usersettings.xml
|
||||
- cacert.pem
|
||||
|
||||
@@ -102,7 +108,10 @@ Notez également que s'il y a des fichiers ".so" dans le répertoire d'Aquaria (
|
||||
`libSDL-1.2.so.0` and `libstdc++.so.6`), vous devriez les retirer. Il s'agit de vieille version des librairies qui
|
||||
ne sont plus fonctionnelles dans les systèmes modernes et qui pourrait empêcher le randomizer de fonctionner.
|
||||
|
||||
Pour lancer le randomizer, utiliser la commande suivante:
|
||||
Pour lancer le randomizer en utilisant le lanceur intégré, seulement exécuter le fichier `aquaria_randomizer`.
|
||||
|
||||
Vous pouvez également lancer le randomizer en spécifiant les information de connexion dans les arguments de la
|
||||
ligne de commande:
|
||||
|
||||
```bash
|
||||
./aquaria_randomizer --name VotreNom --server LeServeur:LePort
|
||||
@@ -120,6 +129,21 @@ pour vous assurer que votre fichier est exécutable:
|
||||
```bash
|
||||
chmod +x aquaria_randomizer
|
||||
```
|
||||
### Steam Deck
|
||||
|
||||
Pour installer le randomizer sur la Steam Deck, seulement suivre la procédure pour les fichiers AppImage
|
||||
indiquée précédemment.
|
||||
|
||||
### Aucun son sur Linux/Steam Deck
|
||||
|
||||
Si le jeu fonctionne sans problème, mais qu'il n'y a aucun son, c'est probablement parce que le jeu
|
||||
n'arrive pas à utiliser le bon pilote de son. Généralement, le problème est réglé en ajoutant la
|
||||
variable d'environnement `ALSOFT_DRIVERS=pulse`. Voici un exemple (peut varier en fonction de la manière
|
||||
que le randomizer est lancé):
|
||||
|
||||
```bash
|
||||
ALSOFT_DRIVERS=pulse ./Aquaria_Randomizer-*.AppImage --name VotreNom --server LeServeur:LePort
|
||||
```
|
||||
|
||||
## Tracking automatique
|
||||
|
||||
|
||||
27
worlds/celeste64/LICENSE
Normal file
27
worlds/celeste64/LICENSE
Normal file
@@ -0,0 +1,27 @@
|
||||
Modified MIT License
|
||||
|
||||
Copyright (c) 2025 PoryGone
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, and/or distribute copies of the Software,
|
||||
and to permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
No copy or substantial portion of the Software shall be sublicensed or relicensed
|
||||
without the express written permission of the copyright holder(s)
|
||||
|
||||
No copy or substantial portion of the Software shall be sold without the express
|
||||
written permission of the copyright holder(s)
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
6
worlds/celeste64/archipelago.json
Normal file
6
worlds/celeste64/archipelago.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"game": "Celeste 64",
|
||||
"authors": [ "PoryGone" ],
|
||||
"minimum_ap_version": "0.6.3",
|
||||
"world_version": "1.3.1"
|
||||
}
|
||||
27
worlds/celeste_open_world/LICENSE
Normal file
27
worlds/celeste_open_world/LICENSE
Normal file
@@ -0,0 +1,27 @@
|
||||
Modified MIT License
|
||||
|
||||
Copyright (c) 2025 PoryGone
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, and/or distribute copies of the Software,
|
||||
and to permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
No copy or substantial portion of the Software shall be sublicensed or relicensed
|
||||
without the express written permission of the copyright holder(s)
|
||||
|
||||
No copy or substantial portion of the Software shall be sold without the express
|
||||
written permission of the copyright holder(s)
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
6
worlds/celeste_open_world/archipelago.json
Normal file
6
worlds/celeste_open_world/archipelago.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"game": "Celeste (Open World)",
|
||||
"authors": [ "PoryGone" ],
|
||||
"minimum_ap_version": "0.6.3",
|
||||
"world_version": "1.0.5"
|
||||
}
|
||||
@@ -14,25 +14,27 @@ The following are required in order to play Civ VI in Archipelago:
|
||||
|
||||
- A copy of the game `Civilization VI` including the two expansions `Rise & Fall` and `Gathering Storm` (both the Steam and Epic version should work).
|
||||
|
||||
## Enabling the tuner
|
||||
|
||||
In the main menu, navigate to the "Game Options" page. On the "Game" menu, make sure that "Tuner (disables achievements)" is enabled.
|
||||
|
||||
## Mod Installation
|
||||
|
||||
1. Download and unzip the latest release of the mod from [GitHub](https://github.com/hesto2/civilization_archipelago_mod/releases/latest).
|
||||
|
||||
2. Copy the folder containing the mod files to your Civ VI mods folder. On Windows, this is usually located at `C:\Users\YOUR_USER\Documents\My Games\Sid Meier's Civilization VI\Mods`. If you use OneDrive, check if the folder is instead located in your OneDrive file structure, and use that path when relevant in future steps.
|
||||
|
||||
3. After the Archipelago host generates a game, you should be given a `.apcivvi` file. Associate the file with the Archipelago Launcher and double click it.
|
||||
3. After the Archipelago host generates a game, you should be given a `.apcivvi` file. You can open it as a zip file, you can do this by either right clicking it and opening it with a program that handles zip files (if you associate that file with the program it will open it with that program in the future by double clicking it), or by right clicking and renaming the file extension from `apcivvi` to `zip` (only works if you are displaying file extensions). You can also associate the file with the Archipelago Launcher and double click it and it will create a folder with the mod files inside of it.
|
||||
|
||||
4. Copy the contents of the new folder it generates (it will have the same name as the `.apcivvi` file) into your Civilization VI Archipelago Mod folder. If double clicking the `.apcivvi` file doesn't generate a folder, you can instead open it as a zip file. You can do this by either right clicking it and opening it with a program that handles zip files, or by right clicking and renaming the file extension from `apcivvi` to `zip`.
|
||||
4. Copy the contents of the zip file or folder it generated (the name of the folder should be the same as the apcivvi file) into your Civilization VI Archipelago Mod folder (there should be five files placed there from the `.apcivvi` file, overwrite if asked).
|
||||
|
||||
5. Place the files generated from the `.apcivvi` in your archipelago mod folder (there should be five files placed there from the apcivvi file, overwrite if asked). Your mod path should look something like `C:\Users\YOUR_USER\Documents\My Games\Sid Meier's Civilization VI\Mods\civilization_archipelago_mod`.
|
||||
5. Your mod path should look something like `C:\Users\YOUR_USER\Documents\My Games\Sid Meier's Civilization VI\Mods\civilization_archipelago_mod`. If everything was done correctly you can now connect to the game.
|
||||
|
||||
## Configuring your game
|
||||
## Connecting to a game
|
||||
|
||||
Make sure you enable the mod in the main title under Additional Content > Mods. When configuring your game, make sure to start the game in the Ancient Era and leave all settings related to starting technologies and civics as the defaults. Other than that, configure difficulty, AI, etc. as you normally would.
|
||||
1. In the main menu, navigate to the "Game Options" page. On the "Game" menu, make sure that "Tuner (disables achievements)" is enabled.
|
||||
|
||||
2. In the main menu, navigate to the "Additional Content" page, then go to "Mods" and make sure the Archipelago mod is enabled.
|
||||
|
||||
3. When starting the game make sure you are on the Gathering Storm ruleset in a Single Player game. Additionally you must start in the ancient era, other settings and game modes can be customised to your own liking. An important thing to note is that settings preset saves the mod list from when you created it, so if you want to use a setting preset with this you must create it after installing the Archipelago mod.
|
||||
|
||||
4. To connect to the room open the Archipelago Launcher, from within the launcher open the Civ6 client and connect to the room. Once connected to the room enter your slot name and if everything went right you should now be connected.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
@@ -51,3 +53,8 @@ Make sure you enable the mod in the main title under Additional Content > Mods.
|
||||
- If you still have any errors make sure the two expansions Rise & Fall and Gathering Storm are active in the mod selector (all the official DLC works without issues but Rise & Fall and Gathering Storm are required for the mod).
|
||||
|
||||
- If boostsanity is enabled and those items are not being sent out but regular techs are, make sure you placed the files from your new room in the mod folder.
|
||||
|
||||
- If you are neither receiving or sending items, make sure you have the correct client open. The client should be the Civ6 and NOT the Text Client.
|
||||
|
||||
- This should be compatible with a lot of other mods, but if you are having issues try disabling all mods other than the Archipelago mod and see if the problem still persists.
|
||||
|
||||
|
||||
27
worlds/dkc3/LICENSE
Normal file
27
worlds/dkc3/LICENSE
Normal file
@@ -0,0 +1,27 @@
|
||||
Modified MIT License
|
||||
|
||||
Copyright (c) 2025 PoryGone
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, and/or distribute copies of the Software,
|
||||
and to permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
No copy or substantial portion of the Software shall be sublicensed or relicensed
|
||||
without the express written permission of the copyright holder(s)
|
||||
|
||||
No copy or substantial portion of the Software shall be sold without the express
|
||||
written permission of the copyright holder(s)
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
6
worlds/dkc3/archipelago.json
Normal file
6
worlds/dkc3/archipelago.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"game": "Donkey Kong Country 3",
|
||||
"authors": [ "PoryGone" ],
|
||||
"minimum_ap_version": "0.6.3",
|
||||
"world_version": "1.1.0"
|
||||
}
|
||||
@@ -136,6 +136,27 @@ are rolling locally, ensure this file is edited to your liking **before** rollin
|
||||
when running the Archipelago Installation software. If you have changed settings in this file, and would like to retain
|
||||
them, you may rename the file to `options.yaml`.
|
||||
|
||||
### Playing with custom worlds
|
||||
|
||||
If you are generating locally, you can play with worlds that are not included in the Archipelago installation.
|
||||
These worlds are packaged as `.apworld` files. To add a world to your installation, click the "Install APWorld" button
|
||||
in the launcher and select the `.apworld` file you wish to install. Alternatively, you can drag the `.apworld` file
|
||||
onto the launcher or double-click the file itself (if on Windows). Once the world is installed, it will function like
|
||||
the worlds that are already packaged with Archipelago. Also note that while generation with custom worlds must be done
|
||||
locally, these games can then be uploaded to the website for hosting and played as normal.
|
||||
|
||||
We strongly recommend that you ensure the source of the `.apworld` is safe and trustworthy before playing with a
|
||||
custom world. Installed APWorlds are able to run custom code on your computer whenever you open Archipelago.
|
||||
|
||||
#### Alternate versions of included worlds
|
||||
|
||||
If you want to play with an alternate version of a game that is already included in Archipelago, you should also
|
||||
remove the original APWorld after completing the above installation. To do so, go to your Archipelago installation
|
||||
folder and navigate to the `lib/worlds` directory. Then move the `.apworld` or the folder corresponding to the game you
|
||||
want to play an alternate version of to somewhere else as a backup. If you want to play this original again, then
|
||||
restore the original version to `lib/worlds` and remove the alternate version, which is in the `custom_worlds` folder.
|
||||
|
||||
Note: Currently, this cannot be done on the Linux AppImage release.
|
||||
|
||||
## Hosting an Archipelago Server
|
||||
|
||||
|
||||
@@ -3,9 +3,6 @@ ModuleUpdate.update()
|
||||
|
||||
import Utils
|
||||
|
||||
if __name__ == "__main__":
|
||||
Utils.init_logging("LinksAwakeningContext", exception_logger="Client")
|
||||
|
||||
import asyncio
|
||||
import base64
|
||||
import binascii
|
||||
@@ -26,16 +23,14 @@ import typing
|
||||
from CommonClient import (CommonContext, get_base_parser, gui_enabled, logger,
|
||||
server_loop)
|
||||
from NetUtils import ClientStatus
|
||||
from worlds.ladx import LinksAwakeningWorld
|
||||
from worlds.ladx.Common import BASE_ID as LABaseID
|
||||
from worlds.ladx.GpsTracker import GpsTracker
|
||||
from worlds.ladx.TrackerConsts import storage_key
|
||||
from worlds.ladx.ItemTracker import ItemTracker
|
||||
from worlds.ladx.LADXR.checkMetadata import checkMetadataTable
|
||||
from worlds.ladx.Locations import get_locations_to_id, meta_to_name
|
||||
from worlds.ladx.Tracker import LocationTracker, MagpieBridge, Check
|
||||
|
||||
|
||||
from . import LinksAwakeningWorld
|
||||
from .Common import BASE_ID as LABaseID
|
||||
from .GpsTracker import GpsTracker
|
||||
from .TrackerConsts import storage_key
|
||||
from .ItemTracker import ItemTracker
|
||||
from .LADXR.checkMetadata import checkMetadataTable
|
||||
from .Locations import get_locations_to_id, meta_to_name
|
||||
from .Tracker import LocationTracker, MagpieBridge, Check
|
||||
class GameboyException(Exception):
|
||||
pass
|
||||
|
||||
@@ -760,42 +755,44 @@ def run_game(romfile: str) -> None:
|
||||
except FileNotFoundError:
|
||||
logger.error(f"Couldn't launch ROM, {args[0]} is missing")
|
||||
|
||||
async def main():
|
||||
parser = get_base_parser(description="Link's Awakening Client.")
|
||||
parser.add_argument("--url", help="Archipelago connection url")
|
||||
parser.add_argument("--no-magpie", dest='magpie', default=True, action='store_false', help="Disable magpie bridge")
|
||||
parser.add_argument('diff_file', default="", type=str, nargs="?",
|
||||
help='Path to a .apladx Archipelago Binary Patch file')
|
||||
def launch(*launch_args):
|
||||
async def main():
|
||||
parser = get_base_parser(description="Link's Awakening Client.")
|
||||
parser.add_argument("--url", help="Archipelago connection url")
|
||||
parser.add_argument("--no-magpie", dest='magpie', default=True, action='store_false', help="Disable magpie bridge")
|
||||
parser.add_argument('diff_file', default="", type=str, nargs="?",
|
||||
help='Path to a .apladx Archipelago Binary Patch file')
|
||||
|
||||
args = parser.parse_args()
|
||||
args = parser.parse_args(launch_args)
|
||||
|
||||
if args.diff_file:
|
||||
import Patch
|
||||
logger.info("patch file was supplied - creating rom...")
|
||||
meta, rom_file = Patch.create_rom_file(args.diff_file)
|
||||
if "server" in meta and not args.connect:
|
||||
args.connect = meta["server"]
|
||||
logger.info(f"wrote rom file to {rom_file}")
|
||||
if args.diff_file:
|
||||
import Patch
|
||||
logger.info("patch file was supplied - creating rom...")
|
||||
meta, rom_file = Patch.create_rom_file(args.diff_file)
|
||||
if "server" in meta and not args.connect:
|
||||
args.connect = meta["server"]
|
||||
logger.info(f"wrote rom file to {rom_file}")
|
||||
|
||||
|
||||
ctx = LinksAwakeningContext(args.connect, args.password, args.magpie)
|
||||
ctx = LinksAwakeningContext(args.connect, args.password, args.magpie)
|
||||
|
||||
ctx.server_task = asyncio.create_task(server_loop(ctx), name="server loop")
|
||||
ctx.server_task = asyncio.create_task(server_loop(ctx), name="server loop")
|
||||
|
||||
# TODO: nothing about the lambda about has to be in a lambda
|
||||
ctx.la_task = create_task_log_exception(ctx.run_game_loop())
|
||||
if gui_enabled:
|
||||
ctx.run_gui()
|
||||
ctx.run_cli()
|
||||
# TODO: nothing about the lambda about has to be in a lambda
|
||||
ctx.la_task = create_task_log_exception(ctx.run_game_loop())
|
||||
if gui_enabled:
|
||||
ctx.run_gui()
|
||||
ctx.run_cli()
|
||||
|
||||
# Down below run_gui so that we get errors out of the process
|
||||
if args.diff_file:
|
||||
run_game(rom_file)
|
||||
# Down below run_gui so that we get errors out of the process
|
||||
if args.diff_file:
|
||||
run_game(rom_file)
|
||||
|
||||
await ctx.exit_event.wait()
|
||||
await ctx.shutdown()
|
||||
await ctx.exit_event.wait()
|
||||
await ctx.shutdown()
|
||||
|
||||
Utils.init_logging("LinksAwakeningContext", exception_logger="Client")
|
||||
|
||||
if __name__ == '__main__':
|
||||
colorama.just_fix_windows_console()
|
||||
asyncio.run(main())
|
||||
colorama.deinit()
|
||||
@@ -3,7 +3,7 @@ from dataclasses import dataclass
|
||||
import os.path
|
||||
import typing
|
||||
import logging
|
||||
from Options import Choice, Toggle, DefaultOnToggle, Range, FreeText, PerGameCommonOptions, OptionGroup, Removed
|
||||
from Options import Choice, Toggle, DefaultOnToggle, Range, FreeText, PerGameCommonOptions, OptionGroup, Removed, StartInventoryPool
|
||||
from collections import defaultdict
|
||||
import Utils
|
||||
|
||||
@@ -665,6 +665,7 @@ class LinksAwakeningOptions(PerGameCommonOptions):
|
||||
tarins_gift: TarinsGift
|
||||
overworld: Overworld
|
||||
stabilize_item_pool: StabilizeItemPool
|
||||
start_inventory_from_pool: StartInventoryPool
|
||||
|
||||
warp_improvements: Removed
|
||||
additional_warp_points: Removed
|
||||
|
||||
@@ -9,6 +9,7 @@ import settings
|
||||
from BaseClasses import CollectionState, Entrance, Item, ItemClassification, Location, Tutorial
|
||||
from Fill import fill_restrictive
|
||||
from worlds.AutoWorld import WebWorld, World
|
||||
from worlds.LauncherComponents import Component, components, SuffixIdentifier, Type, launch, icon_paths
|
||||
from .Common import *
|
||||
from . import ItemIconGuessing
|
||||
from .Items import (DungeonItemData, DungeonItemType, ItemName, LinksAwakeningItem, TradeItemData,
|
||||
@@ -29,6 +30,19 @@ from .Rom import LADXProcedurePatch, write_patch_data
|
||||
DEVELOPER_MODE = False
|
||||
|
||||
|
||||
def launch_client(*args):
|
||||
from .LinksAwakeningClient import launch as ladx_launch
|
||||
launch(ladx_launch, name=f"{LINKS_AWAKENING} Client", args=args)
|
||||
|
||||
components.append(Component(f"{LINKS_AWAKENING} Client",
|
||||
func=launch_client,
|
||||
component_type=Type.CLIENT,
|
||||
icon=LINKS_AWAKENING,
|
||||
file_identifier=SuffixIdentifier('.apladx')))
|
||||
|
||||
icon_paths[LINKS_AWAKENING] = "ap:worlds.ladx/assets/MarinV-3_small.png"
|
||||
|
||||
|
||||
class LinksAwakeningSettings(settings.Group):
|
||||
class RomFile(settings.UserFilePath):
|
||||
"""File name of the Link's Awakening DX rom"""
|
||||
@@ -211,8 +225,6 @@ class LinksAwakeningWorld(World):
|
||||
def create_items(self) -> None:
|
||||
itempool = []
|
||||
|
||||
exclude = [item.name for item in self.multiworld.precollected_items[self.player]]
|
||||
|
||||
self.prefill_original_dungeon = [ [], [], [], [], [], [], [], [], [] ]
|
||||
self.prefill_own_dungeons = []
|
||||
self.pre_fill_items = []
|
||||
@@ -229,50 +241,46 @@ class LinksAwakeningWorld(World):
|
||||
continue
|
||||
item_name = ladxr_item_to_la_item_name[ladx_item_name]
|
||||
for _ in range(count):
|
||||
if item_name in exclude:
|
||||
exclude.remove(item_name) # this is destructive. create unique list above
|
||||
self.multiworld.itempool.append(self.create_item(self.get_filler_item_name()))
|
||||
else:
|
||||
item = self.create_item(item_name)
|
||||
item = self.create_item(item_name)
|
||||
|
||||
if not self.options.tradequest and isinstance(item.item_data, TradeItemData):
|
||||
location = self.multiworld.get_location(item.item_data.vanilla_location, self.player)
|
||||
location.place_locked_item(item)
|
||||
location.show_in_spoiler = False
|
||||
continue
|
||||
if not self.options.tradequest and isinstance(item.item_data, TradeItemData):
|
||||
location = self.multiworld.get_location(item.item_data.vanilla_location, self.player)
|
||||
location.place_locked_item(item)
|
||||
location.show_in_spoiler = False
|
||||
continue
|
||||
|
||||
if isinstance(item.item_data, DungeonItemData):
|
||||
item_type = item.item_data.ladxr_id[:-1]
|
||||
shuffle_type = self.dungeon_item_types[item_type]
|
||||
if isinstance(item.item_data, DungeonItemData):
|
||||
item_type = item.item_data.ladxr_id[:-1]
|
||||
shuffle_type = self.dungeon_item_types[item_type]
|
||||
|
||||
if item.item_data.dungeon_item_type == DungeonItemType.INSTRUMENT and shuffle_type == ShuffleInstruments.option_vanilla:
|
||||
# Find instrument, lock
|
||||
# TODO: we should be able to pinpoint the region we want, save a lookup table please
|
||||
found = False
|
||||
for r in self.multiworld.get_regions(self.player):
|
||||
if r.dungeon_index != item.item_data.dungeon_index:
|
||||
if item.item_data.dungeon_item_type == DungeonItemType.INSTRUMENT and shuffle_type == ShuffleInstruments.option_vanilla:
|
||||
# Find instrument, lock
|
||||
# TODO: we should be able to pinpoint the region we want, save a lookup table please
|
||||
found = False
|
||||
for r in self.multiworld.get_regions(self.player):
|
||||
if r.dungeon_index != item.item_data.dungeon_index:
|
||||
continue
|
||||
for loc in r.locations:
|
||||
if not isinstance(loc, LinksAwakeningLocation):
|
||||
continue
|
||||
for loc in r.locations:
|
||||
if not isinstance(loc, LinksAwakeningLocation):
|
||||
continue
|
||||
if not isinstance(loc.ladxr_item, Instrument):
|
||||
continue
|
||||
loc.place_locked_item(item)
|
||||
found = True
|
||||
break
|
||||
if found:
|
||||
break
|
||||
else:
|
||||
if shuffle_type == DungeonItemShuffle.option_original_dungeon:
|
||||
self.prefill_original_dungeon[item.item_data.dungeon_index - 1].append(item)
|
||||
self.pre_fill_items.append(item)
|
||||
elif shuffle_type == DungeonItemShuffle.option_own_dungeons:
|
||||
self.prefill_own_dungeons.append(item)
|
||||
self.pre_fill_items.append(item)
|
||||
else:
|
||||
itempool.append(item)
|
||||
if not isinstance(loc.ladxr_item, Instrument):
|
||||
continue
|
||||
loc.place_locked_item(item)
|
||||
found = True
|
||||
break
|
||||
if found:
|
||||
break
|
||||
else:
|
||||
itempool.append(item)
|
||||
if shuffle_type == DungeonItemShuffle.option_original_dungeon:
|
||||
self.prefill_original_dungeon[item.item_data.dungeon_index - 1].append(item)
|
||||
self.pre_fill_items.append(item)
|
||||
elif shuffle_type == DungeonItemShuffle.option_own_dungeons:
|
||||
self.prefill_own_dungeons.append(item)
|
||||
self.pre_fill_items.append(item)
|
||||
else:
|
||||
itempool.append(item)
|
||||
else:
|
||||
itempool.append(item)
|
||||
|
||||
self.multi_key = self.generate_multi_key()
|
||||
|
||||
|
||||
BIN
worlds/ladx/assets/MarinV-3.png
Normal file
BIN
worlds/ladx/assets/MarinV-3.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 34 KiB |
BIN
worlds/ladx/assets/MarinV-3_small.png
Normal file
BIN
worlds/ladx/assets/MarinV-3_small.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.7 KiB |
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"game": "Mega Man 2",
|
||||
"world_version": "0.3.2",
|
||||
"world_version": "0.3.3",
|
||||
"minimum_ap_version": "0.6.4"
|
||||
}
|
||||
|
||||
Binary file not shown.
@@ -327,8 +327,6 @@ def patch_rom(world: "MM2World", patch: MM2ProcedurePatch) -> None:
|
||||
patch.write_byte(0x36089, pool[18]) # Intro
|
||||
patch.write_byte(0x361F1, pool[19]) # Title
|
||||
|
||||
|
||||
|
||||
from Utils import __version__
|
||||
patch.name = bytearray(f'MM2{__version__.replace(".", "")[0:3]}_{world.player}_{world.multiworld.seed:11}\0',
|
||||
'utf8')[:21]
|
||||
|
||||
@@ -58,6 +58,10 @@ FlashFixTarget1:
|
||||
%org($808D, $0B)
|
||||
FlashFixTarget2:
|
||||
|
||||
%org($A65C, $0B)
|
||||
HeatFix:
|
||||
CMP #$FF
|
||||
|
||||
%org($8015, $0D)
|
||||
ClearRefreshHook:
|
||||
; if we're already doing a fresh load of the stage select
|
||||
|
||||
@@ -36,6 +36,26 @@ DATA_LOCATIONS = {
|
||||
"CrashCheck4": (0x16DD, 1),
|
||||
}
|
||||
|
||||
TRACKER_EVENT_FLAGS = [
|
||||
0x77, # EVENT_BEAT_BROCK
|
||||
0xbf, # EVENT_BEAT_MISTY
|
||||
0x167, # EVENT_BEAT_LT_SURGE
|
||||
0x1a9, # EVENT_BEAT_ERIKA
|
||||
0x259, # EVENT_BEAT_KOGA
|
||||
0x361, # EVENT_BEAT_SABRINA
|
||||
0x299, # EVENT_BEAT_BLAINE
|
||||
0x51, # EVENT_BEAT_VIRIDIAN_GYM_GIOVANNI
|
||||
|
||||
0x38, # EVENT_OAK_GOT_PARCEL
|
||||
0x525, # EVENT_BEAT_ROUTE22_RIVAL_1ST_BATTLE
|
||||
0x117, # EVENT_RESCUED_MR_FUJI
|
||||
0x55c, # EVENT_GOT_SS_TICKET
|
||||
0x78f, # EVENT_BEAT_SILPH_CO_GIOVANNI
|
||||
0x901, # EVENT_BEAT_CHAMPION_RIVAL
|
||||
]
|
||||
|
||||
assert len(TRACKER_EVENT_FLAGS) <= 32
|
||||
|
||||
location_map = {"Rod": {}, "EventFlag": {}, "Missable": {}, "Hidden": {}, "list": {}, "DexSanityFlag": {}}
|
||||
location_bytes_bits = {}
|
||||
for location in location_data:
|
||||
@@ -61,6 +81,7 @@ class PokemonRBClient(BizHawkClient):
|
||||
super().__init__()
|
||||
self.auto_hints = set()
|
||||
self.locations_array = None
|
||||
self.tracker_bitfield = 0
|
||||
self.disconnect_pending = False
|
||||
self.set_deathlink = False
|
||||
self.banking_command = None
|
||||
@@ -236,6 +257,22 @@ class PokemonRBClient(BizHawkClient):
|
||||
await ctx.send_msgs([{"cmd": "Bounce", "slots": [ctx.slot], "data": {"currentMap": data["CurrentMap"][0]}}])
|
||||
self.current_map = data["CurrentMap"][0]
|
||||
|
||||
# TRACKER
|
||||
tracker_bitfield = 0
|
||||
for i, flag in enumerate(TRACKER_EVENT_FLAGS):
|
||||
if data["EventFlag"][flag // 8] & (1 << (flag % 8)):
|
||||
tracker_bitfield |= 1 << i
|
||||
|
||||
if tracker_bitfield != self.tracker_bitfield:
|
||||
await ctx.send_msgs([{
|
||||
"cmd": "Set",
|
||||
"key": f"pokemon_rb_events_{ctx.team}_{ctx.slot}",
|
||||
"default": 0,
|
||||
"want_reply": False,
|
||||
"operations": [{"operation": "or", "value": tracker_bitfield}],
|
||||
}])
|
||||
self.tracker_bitfield = tracker_bitfield
|
||||
|
||||
# VICTORY
|
||||
|
||||
if data["EventFlag"][280] & 1 and not ctx.finished_game:
|
||||
|
||||
28
worlds/sa2b/LICENSE
Normal file
28
worlds/sa2b/LICENSE
Normal file
@@ -0,0 +1,28 @@
|
||||
Modified MIT License
|
||||
|
||||
Copyright (c) 2025 PoryGone
|
||||
Copyright (c) 2025 RaspberrySpaceJam
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, and/or distribute copies of the Software,
|
||||
and to permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
No copy or substantial portion of the Software shall be sublicensed or relicensed
|
||||
without the express written permission of the copyright holder(s)
|
||||
|
||||
No copy or substantial portion of the Software shall be sold without the express
|
||||
written permission of the copyright holder(s)
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
6
worlds/sa2b/archipelago.json
Normal file
6
worlds/sa2b/archipelago.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"game": "Sonic Adventure 2 Battle",
|
||||
"authors": [ "PoryGone", "RaspberrySpaceJam" ],
|
||||
"minimum_ap_version": "0.6.3",
|
||||
"world_version": "2.4.2"
|
||||
}
|
||||
@@ -35,6 +35,7 @@ from .mission_tables import SC2Campaign, SC2Mission, SC2Race, MissionFlag
|
||||
from .regions import create_mission_order
|
||||
from .mission_order import SC2MissionOrder
|
||||
from worlds.LauncherComponents import components, Component, launch as launch_component
|
||||
from .mission_order.presets import sc2_options_presets
|
||||
|
||||
logger = logging.getLogger("Starcraft 2")
|
||||
VICTORY_MODULO = 100
|
||||
@@ -75,6 +76,7 @@ class Starcraft2WebWorld(WebWorld):
|
||||
|
||||
tutorials = [setup_en, setup_fr, custom_mission_orders_en]
|
||||
game_info_languages = ["en", "fr"]
|
||||
options_presets = sc2_options_presets
|
||||
option_groups = option_groups
|
||||
|
||||
|
||||
|
||||
446
worlds/sc2/mission_order/presets.py
Normal file
446
worlds/sc2/mission_order/presets.py
Normal file
@@ -0,0 +1,446 @@
|
||||
from typing import Any, Dict
|
||||
|
||||
import Options as ap_options
|
||||
from .. import options
|
||||
from Options import Accessibility, ProgressionBalancing
|
||||
from .. import item_names
|
||||
from ..mission_tables import SC2Race, SC2Campaign
|
||||
|
||||
from ..options import (
|
||||
# avoid import *
|
||||
GameDifficulty, DifficultyDamageModifier, GameSpeed, DisableForcedCamera, SkipCutscenes, AllInMap, MissionOrder,
|
||||
MaximumCampaignSize, TwoStartPositions, KeyMode, PlayerColorTerranRaynor, PlayerColorProtoss, PlayerColorZerg,
|
||||
PlayerColorZergPrimal, PlayerColorNova, SelectedRaces, EnabledCampaigns, EnableRaceSwapVariants, EnableMissionRaceBalancing,
|
||||
ShuffleCampaigns, ShuffleNoBuild, StarterUnit, RequiredTactics, EnableVoidTrade, VoidTradeAgeLimit, VoidTradeWorkers,
|
||||
EnsureGenericItems, MinNumberOfUpgrades, MaxNumberOfUpgrades, MercenaryHighlanders, MaxUpgradeLevel, GenericUpgradeMissions,
|
||||
GenericUpgradeResearch, GenericUpgradeResearchSpeedup, GenericUpgradeItems, KerriganPresence, KerriganLevelsPerMissionCompleted,
|
||||
KerriganLevelsPerMissionCompletedCap, KerriganLevelItemSum, KerriganLevelItemDistribution, KerriganTotalLevelCap, StartPrimaryAbilities,
|
||||
KerriganPrimalStatus, KerriganMaxActiveAbilities, KerriganMaxPassiveAbilities, EnableMorphling, WarCouncilNerfs, SpearOfAdunPresence,
|
||||
SpearOfAdunPresentInNoBuild, SpearOfAdunPassiveAbilityPresence, SpearOfAdunPassivesPresentInNoBuild, SpearOfAdunMaxActiveAbilities,
|
||||
SpearOfAdunMaxAutocastAbilities, GrantStoryTech, GrantStoryLevels, NovaMaxWeapons, NovaMaxGadgets, NovaGhostOfAChanceVariant,
|
||||
TakeOverAIAllies, LockedItems, ExcludedItems, UnexcludedItems, ExcludedMissions, DifficultyCurve, ExcludeVeryHardMissions, VanillaItemsOnly,
|
||||
ExcludeOverpoweredItems, VictoryCache, VanillaLocations, ExtraLocations, ChallengeLocations, MasteryLocations, BasebustLocations,
|
||||
SpeedrunLocations, PreventativeLocations, FillerPercentage, MineralsPerItem, VespenePerItem, StartingSupplyPerItem, MaximumSupplyPerItem,
|
||||
MaximumSupplyReductionPerItem, LowestMaximumSupply, ResearchCostReductionPerItem, FillerItemsDistribution, MissionOrderScouting,
|
||||
CustomMissionOrder, OPTION_NAME
|
||||
)
|
||||
|
||||
template_settings = {
|
||||
# Free to play, 25 WoL missions, Terran only Golden Path. Matches the template.yaml.
|
||||
OPTION_NAME[Accessibility]: Accessibility.option_full,
|
||||
OPTION_NAME[ProgressionBalancing]: ProgressionBalancing.default,
|
||||
OPTION_NAME[GameDifficulty]: GameDifficulty.option_normal,
|
||||
OPTION_NAME[SelectedRaces]: {SC2Race.TERRAN.get_title()},
|
||||
OPTION_NAME[MissionOrder]: MissionOrder.option_golden_path,
|
||||
OPTION_NAME[RequiredTactics]: RequiredTactics.option_standard,
|
||||
OPTION_NAME[EnabledCampaigns]: {SC2Campaign.WOL.campaign_name},
|
||||
OPTION_NAME[EnableRaceSwapVariants]: EnableRaceSwapVariants.option_disabled,
|
||||
OPTION_NAME[KeyMode]: KeyMode.option_disabled,
|
||||
OPTION_NAME[MaximumCampaignSize]: 25,
|
||||
OPTION_NAME[StarterUnit]: StarterUnit.option_balanced,
|
||||
OPTION_NAME[NovaGhostOfAChanceVariant]: NovaGhostOfAChanceVariant.option_wol,
|
||||
OPTION_NAME[TakeOverAIAllies]: TakeOverAIAllies.option_false,
|
||||
OPTION_NAME[DifficultyCurve]: DifficultyCurve.option_standard,
|
||||
OPTION_NAME[MasteryLocations]: MasteryLocations.option_disabled,
|
||||
OPTION_NAME[MissionOrderScouting]: MissionOrderScouting.option_completed,
|
||||
OPTION_NAME[MineralsPerItem]: 10,
|
||||
OPTION_NAME[VespenePerItem]: 10,
|
||||
OPTION_NAME[StartingSupplyPerItem]: 2,
|
||||
OPTION_NAME[MaximumSupplyPerItem]: 1,
|
||||
OPTION_NAME[ResearchCostReductionPerItem]: 2,
|
||||
}
|
||||
|
||||
|
||||
|
||||
f2p_big_settings = {
|
||||
# Free to play, all race 7x7 Grid
|
||||
OPTION_NAME[Accessibility]: Accessibility.option_full,
|
||||
OPTION_NAME[ProgressionBalancing]: ProgressionBalancing.default,
|
||||
OPTION_NAME[GameDifficulty]: GameDifficulty.option_normal,
|
||||
OPTION_NAME[SelectedRaces]: SelectedRaces.valid_keys,
|
||||
OPTION_NAME[MissionOrder]: MissionOrder.option_grid,
|
||||
OPTION_NAME[RequiredTactics]: RequiredTactics.option_standard,
|
||||
OPTION_NAME[EnabledCampaigns]: {
|
||||
SC2Campaign.WOL.campaign_name,
|
||||
SC2Campaign.PROPHECY.campaign_name,
|
||||
SC2Campaign.PROLOGUE.campaign_name
|
||||
},
|
||||
OPTION_NAME[EnableRaceSwapVariants]: EnableRaceSwapVariants.option_shuffle_all,
|
||||
OPTION_NAME[EnableMissionRaceBalancing]: EnableMissionRaceBalancing.option_semi_balanced,
|
||||
OPTION_NAME[KeyMode]: KeyMode.option_disabled,
|
||||
OPTION_NAME[MaximumCampaignSize]: 48,
|
||||
OPTION_NAME[TwoStartPositions]: TwoStartPositions.option_true,
|
||||
OPTION_NAME[StarterUnit]: StarterUnit.option_balanced,
|
||||
OPTION_NAME[EnableMorphling]: EnableMorphling.option_true,
|
||||
OPTION_NAME[TakeOverAIAllies]: TakeOverAIAllies.option_false,
|
||||
OPTION_NAME[DifficultyCurve]: DifficultyCurve.option_standard,
|
||||
OPTION_NAME[VanillaLocations]: VanillaLocations.option_enabled,
|
||||
OPTION_NAME[ExtraLocations]: ExtraLocations.option_enabled,
|
||||
OPTION_NAME[ChallengeLocations]: ChallengeLocations.option_enabled,
|
||||
OPTION_NAME[MasteryLocations]: MasteryLocations.option_disabled,
|
||||
OPTION_NAME[KerriganPrimalStatus]: KerriganPrimalStatus.option_item,
|
||||
OPTION_NAME[WarCouncilNerfs]: WarCouncilNerfs.option_false,
|
||||
OPTION_NAME[SpearOfAdunPresence]: SpearOfAdunPresence.option_everywhere,
|
||||
OPTION_NAME[SpearOfAdunPresentInNoBuild]: SpearOfAdunPresentInNoBuild.option_false,
|
||||
OPTION_NAME[SpearOfAdunPassiveAbilityPresence]: SpearOfAdunPassiveAbilityPresence.option_everywhere,
|
||||
OPTION_NAME[SpearOfAdunPassivesPresentInNoBuild]: SpearOfAdunPassivesPresentInNoBuild.option_false,
|
||||
OPTION_NAME[MissionOrderScouting]: MissionOrderScouting.option_completed,
|
||||
OPTION_NAME[MineralsPerItem]: 5,
|
||||
OPTION_NAME[VespenePerItem]: 5,
|
||||
OPTION_NAME[StartingSupplyPerItem]: 2,
|
||||
OPTION_NAME[MaximumSupplyPerItem]: 1,
|
||||
OPTION_NAME[ResearchCostReductionPerItem]: 2,
|
||||
}
|
||||
|
||||
zerg_rush_settings = {
|
||||
# Zerg only, Blitz, short (5 required, 15 total). Sync-friendly
|
||||
OPTION_NAME[Accessibility]: Accessibility.option_full,
|
||||
OPTION_NAME[ProgressionBalancing]: ProgressionBalancing.default,
|
||||
OPTION_NAME[GameDifficulty]: GameDifficulty.option_hard,
|
||||
OPTION_NAME[SelectedRaces]: {SC2Race.ZERG.get_title()},
|
||||
OPTION_NAME[MissionOrder]: MissionOrder.option_blitz,
|
||||
OPTION_NAME[RequiredTactics]: RequiredTactics.option_advanced,
|
||||
OPTION_NAME[EnabledCampaigns]: EnabledCampaigns.valid_keys,
|
||||
OPTION_NAME[EnableRaceSwapVariants]: EnableRaceSwapVariants.option_shuffle_all,
|
||||
OPTION_NAME[KeyMode]: KeyMode.option_disabled,
|
||||
OPTION_NAME[MaximumCampaignSize]: 15,
|
||||
OPTION_NAME[StarterUnit]: StarterUnit.option_balanced,
|
||||
OPTION_NAME[EnableMorphling]: EnableMorphling.option_true,
|
||||
OPTION_NAME[TakeOverAIAllies]: TakeOverAIAllies.option_false,
|
||||
OPTION_NAME[DifficultyCurve]: DifficultyCurve.option_standard,
|
||||
OPTION_NAME[VictoryCache]: 3,
|
||||
OPTION_NAME[GenericUpgradeItems]: GenericUpgradeItems.option_bundle_all,
|
||||
OPTION_NAME[VanillaLocations]: VanillaLocations.option_enabled,
|
||||
OPTION_NAME[ExtraLocations]: ExtraLocations.option_enabled,
|
||||
OPTION_NAME[ChallengeLocations]: ChallengeLocations.option_enabled,
|
||||
OPTION_NAME[MasteryLocations]: MasteryLocations.option_disabled,
|
||||
OPTION_NAME[ExcludeVeryHardMissions]: ExcludeVeryHardMissions.option_true,
|
||||
OPTION_NAME[KerriganPrimalStatus]: KerriganPrimalStatus.option_item,
|
||||
OPTION_NAME[MissionOrderScouting]: MissionOrderScouting.option_all,
|
||||
OPTION_NAME[GrantStoryTech]: GrantStoryTech.option_grant,
|
||||
OPTION_NAME[GrantStoryLevels]: GrantStoryLevels.option_minimum,
|
||||
OPTION_NAME[MineralsPerItem]: 25,
|
||||
OPTION_NAME[VespenePerItem]: 25,
|
||||
OPTION_NAME[StartingSupplyPerItem]: 5,
|
||||
OPTION_NAME[MaximumSupplyPerItem]: 2,
|
||||
OPTION_NAME[ResearchCostReductionPerItem]: 5,
|
||||
}
|
||||
|
||||
classic_grid_settings = {
|
||||
# Short-ish 5x5 Grid (8 required, 24 total), all races. Sync-friendly
|
||||
OPTION_NAME[Accessibility]: Accessibility.option_full,
|
||||
OPTION_NAME[ProgressionBalancing]: ProgressionBalancing.default,
|
||||
OPTION_NAME[GameDifficulty]: GameDifficulty.option_hard,
|
||||
OPTION_NAME[SelectedRaces]: SelectedRaces.valid_keys,
|
||||
OPTION_NAME[MissionOrder]: MissionOrder.option_grid,
|
||||
OPTION_NAME[RequiredTactics]: RequiredTactics.option_standard,
|
||||
OPTION_NAME[EnabledCampaigns]: EnabledCampaigns.valid_keys,
|
||||
OPTION_NAME[EnableRaceSwapVariants]: EnableRaceSwapVariants.option_shuffle_all,
|
||||
OPTION_NAME[EnableMissionRaceBalancing]: EnableMissionRaceBalancing.option_fully_balanced,
|
||||
OPTION_NAME[KeyMode]: KeyMode.option_disabled,
|
||||
OPTION_NAME[MaximumCampaignSize]: 24,
|
||||
OPTION_NAME[TwoStartPositions]: TwoStartPositions.option_true,
|
||||
OPTION_NAME[StarterUnit]: StarterUnit.option_balanced,
|
||||
OPTION_NAME[EnableMorphling]: EnableMorphling.option_true,
|
||||
OPTION_NAME[TakeOverAIAllies]: TakeOverAIAllies.option_false,
|
||||
OPTION_NAME[DifficultyCurve]: DifficultyCurve.option_standard,
|
||||
OPTION_NAME[VictoryCache]: 5,
|
||||
OPTION_NAME[GenericUpgradeItems]: GenericUpgradeItems.option_bundle_all,
|
||||
OPTION_NAME[VanillaLocations]: VanillaLocations.option_enabled,
|
||||
OPTION_NAME[ExtraLocations]: ExtraLocations.option_enabled,
|
||||
OPTION_NAME[ChallengeLocations]: ChallengeLocations.option_enabled,
|
||||
OPTION_NAME[MasteryLocations]: MasteryLocations.option_disabled,
|
||||
OPTION_NAME[ExcludeVeryHardMissions]: ExcludeVeryHardMissions.option_true,
|
||||
OPTION_NAME[KerriganPrimalStatus]: KerriganPrimalStatus.option_item,
|
||||
OPTION_NAME[WarCouncilNerfs]: WarCouncilNerfs.option_false,
|
||||
OPTION_NAME[SpearOfAdunPresence]: SpearOfAdunPresence.option_any_race_lotv,
|
||||
OPTION_NAME[SpearOfAdunPresentInNoBuild]: SpearOfAdunPresentInNoBuild.option_false,
|
||||
OPTION_NAME[SpearOfAdunPassiveAbilityPresence]: SpearOfAdunPassiveAbilityPresence.option_any_race_lotv,
|
||||
OPTION_NAME[SpearOfAdunPassivesPresentInNoBuild]: SpearOfAdunPassivesPresentInNoBuild.option_false,
|
||||
OPTION_NAME[MissionOrderScouting]: MissionOrderScouting.option_all,
|
||||
OPTION_NAME[GrantStoryTech]: GrantStoryTech.option_grant,
|
||||
OPTION_NAME[GrantStoryLevels]: GrantStoryLevels.option_minimum,
|
||||
OPTION_NAME[MineralsPerItem]: 25,
|
||||
OPTION_NAME[VespenePerItem]: 25,
|
||||
OPTION_NAME[StartingSupplyPerItem]: 5,
|
||||
OPTION_NAME[MaximumSupplyPerItem]: 2,
|
||||
OPTION_NAME[ResearchCostReductionPerItem]: 5,
|
||||
}
|
||||
|
||||
raceswap_blitz_settings = {
|
||||
# medium-sized Blitz (10 required, 50 total), all races, forced raceswaps
|
||||
OPTION_NAME[Accessibility]: Accessibility.option_full,
|
||||
OPTION_NAME[ProgressionBalancing]: ProgressionBalancing.default,
|
||||
OPTION_NAME[GameDifficulty]: GameDifficulty.option_hard,
|
||||
OPTION_NAME[SelectedRaces]: SelectedRaces.valid_keys,
|
||||
OPTION_NAME[MissionOrder]: MissionOrder.option_blitz,
|
||||
OPTION_NAME[RequiredTactics]: RequiredTactics.option_advanced,
|
||||
OPTION_NAME[EnabledCampaigns]: EnabledCampaigns.valid_keys,
|
||||
OPTION_NAME[EnableRaceSwapVariants]: EnableRaceSwapVariants.option_pick_one_non_vanilla,
|
||||
OPTION_NAME[EnableMissionRaceBalancing]: EnableMissionRaceBalancing.option_semi_balanced,
|
||||
OPTION_NAME[KeyMode]: KeyMode.option_disabled,
|
||||
OPTION_NAME[MaximumCampaignSize]: 50,
|
||||
OPTION_NAME[StarterUnit]: StarterUnit.option_off,
|
||||
OPTION_NAME[EnableMorphling]: EnableMorphling.option_true,
|
||||
OPTION_NAME[TakeOverAIAllies]: TakeOverAIAllies.option_false,
|
||||
OPTION_NAME[DifficultyCurve]: DifficultyCurve.option_standard,
|
||||
OPTION_NAME[VanillaLocations]: VanillaLocations.option_enabled,
|
||||
OPTION_NAME[ExtraLocations]: ExtraLocations.option_enabled,
|
||||
OPTION_NAME[ChallengeLocations]: ChallengeLocations.option_enabled,
|
||||
OPTION_NAME[MasteryLocations]: MasteryLocations.option_disabled,
|
||||
OPTION_NAME[KerriganPrimalStatus]: KerriganPrimalStatus.option_item,
|
||||
OPTION_NAME[WarCouncilNerfs]: WarCouncilNerfs.option_true,
|
||||
OPTION_NAME[SpearOfAdunPresence]: SpearOfAdunPresence.option_protoss,
|
||||
OPTION_NAME[SpearOfAdunPresentInNoBuild]: SpearOfAdunPresentInNoBuild.option_false,
|
||||
OPTION_NAME[SpearOfAdunPassiveAbilityPresence]: SpearOfAdunPassiveAbilityPresence.option_protoss,
|
||||
OPTION_NAME[SpearOfAdunPassivesPresentInNoBuild]: SpearOfAdunPassivesPresentInNoBuild.option_false,
|
||||
OPTION_NAME[ExcludeOverpoweredItems]: ExcludeOverpoweredItems.option_true,
|
||||
OPTION_NAME[MissionOrderScouting]: MissionOrderScouting.option_completed,
|
||||
OPTION_NAME[MineralsPerItem]: 10,
|
||||
OPTION_NAME[VespenePerItem]: 10,
|
||||
OPTION_NAME[StartingSupplyPerItem]: 2,
|
||||
OPTION_NAME[MaximumSupplyPerItem]: 1,
|
||||
OPTION_NAME[ResearchCostReductionPerItem]: 2,
|
||||
}
|
||||
|
||||
bread_and_butter_settings = {
|
||||
# 50 mission Golden Path, all races, limits on Upgrades per Unit/Kerrigan/Nova/SoA
|
||||
OPTION_NAME[Accessibility]: Accessibility.option_full,
|
||||
OPTION_NAME[ProgressionBalancing]: ProgressionBalancing.default,
|
||||
OPTION_NAME[GameDifficulty]: GameDifficulty.option_normal,
|
||||
OPTION_NAME[SelectedRaces]: SelectedRaces.valid_keys,
|
||||
OPTION_NAME[MissionOrder]: MissionOrder.option_blitz,
|
||||
OPTION_NAME[RequiredTactics]: RequiredTactics.option_standard,
|
||||
OPTION_NAME[EnabledCampaigns]: EnabledCampaigns.valid_keys,
|
||||
OPTION_NAME[EnableRaceSwapVariants]: EnableRaceSwapVariants.option_pick_one,
|
||||
OPTION_NAME[EnableMissionRaceBalancing]: EnableMissionRaceBalancing.option_semi_balanced,
|
||||
OPTION_NAME[KeyMode]: KeyMode.option_progressive_questlines,
|
||||
OPTION_NAME[MaximumCampaignSize]: 50,
|
||||
OPTION_NAME[StarterUnit]: StarterUnit.option_off,
|
||||
OPTION_NAME[EnableMorphling]: EnableMorphling.option_true,
|
||||
OPTION_NAME[TakeOverAIAllies]: TakeOverAIAllies.option_false,
|
||||
OPTION_NAME[DifficultyCurve]: DifficultyCurve.option_standard,
|
||||
OPTION_NAME[VanillaLocations]: VanillaLocations.option_enabled,
|
||||
OPTION_NAME[ExtraLocations]: ExtraLocations.option_enabled,
|
||||
OPTION_NAME[ChallengeLocations]: ChallengeLocations.option_enabled,
|
||||
OPTION_NAME[MasteryLocations]: MasteryLocations.option_disabled,
|
||||
OPTION_NAME[WarCouncilNerfs]: WarCouncilNerfs.option_true,
|
||||
OPTION_NAME[NovaGhostOfAChanceVariant]: NovaGhostOfAChanceVariant.option_nco,
|
||||
OPTION_NAME[GenericUpgradeItems]: GenericUpgradeItems.option_individual_items,
|
||||
OPTION_NAME[MinNumberOfUpgrades]: 1,
|
||||
OPTION_NAME[MaxNumberOfUpgrades]: 4,
|
||||
OPTION_NAME[NovaMaxWeapons]: 2,
|
||||
OPTION_NAME[NovaMaxGadgets]: 2,
|
||||
OPTION_NAME[KerriganPrimalStatus]: KerriganPrimalStatus.option_item,
|
||||
OPTION_NAME[KerriganMaxActiveAbilities]: 4,
|
||||
OPTION_NAME[KerriganMaxPassiveAbilities]: 2,
|
||||
OPTION_NAME[SpearOfAdunPresence]: SpearOfAdunPresence.option_protoss,
|
||||
OPTION_NAME[SpearOfAdunPresentInNoBuild]: SpearOfAdunPresentInNoBuild.option_false,
|
||||
OPTION_NAME[SpearOfAdunPassiveAbilityPresence]: SpearOfAdunPassiveAbilityPresence.option_protoss,
|
||||
OPTION_NAME[SpearOfAdunPassivesPresentInNoBuild]: SpearOfAdunPassivesPresentInNoBuild.option_false,
|
||||
OPTION_NAME[SpearOfAdunMaxActiveAbilities]: 3,
|
||||
OPTION_NAME[SpearOfAdunMaxAutocastAbilities]: 1,
|
||||
OPTION_NAME[MissionOrderScouting]: MissionOrderScouting.option_completed,
|
||||
OPTION_NAME[ExcludeOverpoweredItems]: ExcludeOverpoweredItems.option_true,
|
||||
OPTION_NAME[GrantStoryTech]: GrantStoryTech.option_allow_substitutes,
|
||||
OPTION_NAME[MineralsPerItem]: 10,
|
||||
OPTION_NAME[VespenePerItem]: 10,
|
||||
OPTION_NAME[StartingSupplyPerItem]: 2,
|
||||
OPTION_NAME[MaximumSupplyPerItem]: 1,
|
||||
OPTION_NAME[ResearchCostReductionPerItem]: 2,
|
||||
OPTION_NAME[FillerPercentage]: 30,
|
||||
OPTION_NAME[FillerItemsDistribution]: {
|
||||
item_names.STARTING_MINERALS: 10,
|
||||
item_names.STARTING_VESPENE: 10,
|
||||
item_names.STARTING_SUPPLY: 10,
|
||||
item_names.MAX_SUPPLY: 10,
|
||||
item_names.SHIELD_REGENERATION: 5,
|
||||
item_names.BUILDING_CONSTRUCTION_SPEED: 10,
|
||||
item_names.KERRIGAN_LEVELS_1: 0,
|
||||
item_names.UPGRADE_RESEARCH_SPEED: 10,
|
||||
item_names.UPGRADE_RESEARCH_COST: 10,
|
||||
item_names.REDUCED_MAX_SUPPLY: 0,
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
all_protoss_settings = {
|
||||
# Vanilla campaign, but full Protoss. 62 missions, huge for one race, reduced locations, low values for fillers
|
||||
OPTION_NAME[Accessibility]: Accessibility.option_full,
|
||||
OPTION_NAME[ProgressionBalancing]: ProgressionBalancing.default,
|
||||
OPTION_NAME[GameDifficulty]: GameDifficulty.option_normal,
|
||||
OPTION_NAME[SelectedRaces]: {SC2Race.PROTOSS.get_title()},
|
||||
OPTION_NAME[MissionOrder]: MissionOrder.option_vanilla_shuffled,
|
||||
OPTION_NAME[RequiredTactics]: RequiredTactics.option_standard,
|
||||
OPTION_NAME[EnabledCampaigns]: {
|
||||
SC2Campaign.WOL.campaign_name,
|
||||
SC2Campaign.PROPHECY.campaign_name,
|
||||
SC2Campaign.HOTS.campaign_name,
|
||||
SC2Campaign.PROLOGUE.campaign_name,
|
||||
SC2Campaign.LOTV.campaign_name,
|
||||
# SC2Campaign.EPILOGUE.campaign_name, enable once Epilogue gets race-swaps
|
||||
# SC2Campaign.NCO.campaign_name, enable once NCO gets race-swaps
|
||||
},
|
||||
OPTION_NAME[EnableRaceSwapVariants]: EnableRaceSwapVariants.option_shuffle_all,
|
||||
OPTION_NAME[KeyMode]: KeyMode.option_progressive_questlines,
|
||||
OPTION_NAME[MaximumCampaignSize]: 61,
|
||||
OPTION_NAME[StarterUnit]: StarterUnit.option_off,
|
||||
OPTION_NAME[GrantStoryTech]: GrantStoryTech.option_grant,
|
||||
OPTION_NAME[GrantStoryLevels]: GrantStoryLevels.option_minimum,
|
||||
OPTION_NAME[TakeOverAIAllies]: TakeOverAIAllies.option_false,
|
||||
OPTION_NAME[DifficultyCurve]: DifficultyCurve.option_standard,
|
||||
OPTION_NAME[VanillaLocations]: VanillaLocations.option_enabled,
|
||||
OPTION_NAME[ExtraLocations]: ExtraLocations.option_enabled,
|
||||
OPTION_NAME[ChallengeLocations]: ChallengeLocations.option_disabled,
|
||||
OPTION_NAME[MasteryLocations]: MasteryLocations.option_disabled,
|
||||
OPTION_NAME[WarCouncilNerfs]: WarCouncilNerfs.option_true,
|
||||
OPTION_NAME[GenericUpgradeItems]: GenericUpgradeItems.option_individual_items,
|
||||
OPTION_NAME[MinNumberOfUpgrades]: 2,
|
||||
OPTION_NAME[MaxNumberOfUpgrades]: 4,
|
||||
OPTION_NAME[SpearOfAdunPresence]: SpearOfAdunPresence.option_protoss,
|
||||
OPTION_NAME[SpearOfAdunPresentInNoBuild]: SpearOfAdunPresentInNoBuild.option_false,
|
||||
OPTION_NAME[SpearOfAdunPassiveAbilityPresence]: SpearOfAdunPassiveAbilityPresence.option_protoss,
|
||||
OPTION_NAME[SpearOfAdunPassivesPresentInNoBuild]: SpearOfAdunPassivesPresentInNoBuild.option_false,
|
||||
OPTION_NAME[SpearOfAdunMaxActiveAbilities]: 3,
|
||||
OPTION_NAME[SpearOfAdunMaxAutocastAbilities]: 1,
|
||||
OPTION_NAME[MissionOrderScouting]: MissionOrderScouting.option_completed,
|
||||
OPTION_NAME[MineralsPerItem]: 5,
|
||||
OPTION_NAME[VespenePerItem]: 5,
|
||||
OPTION_NAME[StartingSupplyPerItem]: 1,
|
||||
OPTION_NAME[MaximumSupplyPerItem]: 1,
|
||||
OPTION_NAME[ResearchCostReductionPerItem]: 1,
|
||||
OPTION_NAME[FillerPercentage]: 30,
|
||||
OPTION_NAME[FillerItemsDistribution]: {
|
||||
item_names.STARTING_MINERALS: 1,
|
||||
item_names.STARTING_VESPENE: 1,
|
||||
item_names.STARTING_SUPPLY: 1,
|
||||
item_names.MAX_SUPPLY: 1,
|
||||
item_names.SHIELD_REGENERATION: 1,
|
||||
item_names.BUILDING_CONSTRUCTION_SPEED: 1,
|
||||
item_names.KERRIGAN_LEVELS_1: 0,
|
||||
item_names.UPGRADE_RESEARCH_SPEED: 1,
|
||||
item_names.UPGRADE_RESEARCH_COST: 1,
|
||||
item_names.REDUCED_MAX_SUPPLY: 0,
|
||||
}
|
||||
}
|
||||
|
||||
evil_logic_settings = {
|
||||
# 6x6 grid on any_unit. High difficulty, disabled Kerrigan, harsh limits, trap items. any_unit has a chance to be unbeatable
|
||||
OPTION_NAME[Accessibility]: Accessibility.option_full,
|
||||
OPTION_NAME[ProgressionBalancing]: ProgressionBalancing.default,
|
||||
OPTION_NAME[GameDifficulty]: GameDifficulty.option_brutal,
|
||||
OPTION_NAME[SelectedRaces]: SelectedRaces.valid_keys,
|
||||
OPTION_NAME[MissionOrder]: MissionOrder.option_grid,
|
||||
OPTION_NAME[RequiredTactics]: RequiredTactics.option_standard,
|
||||
OPTION_NAME[EnabledCampaigns]: EnabledCampaigns.valid_keys,
|
||||
OPTION_NAME[EnableRaceSwapVariants]: EnableRaceSwapVariants.option_pick_one,
|
||||
OPTION_NAME[EnableMissionRaceBalancing]: EnableMissionRaceBalancing.option_semi_balanced,
|
||||
OPTION_NAME[KeyMode]: KeyMode.option_progressive_questlines,
|
||||
OPTION_NAME[MaximumCampaignSize]: 35,
|
||||
OPTION_NAME[StarterUnit]: StarterUnit.option_off,
|
||||
OPTION_NAME[EnableMorphling]: EnableMorphling.option_true,
|
||||
OPTION_NAME[TakeOverAIAllies]: TakeOverAIAllies.option_false,
|
||||
OPTION_NAME[DifficultyCurve]: DifficultyCurve.option_standard,
|
||||
OPTION_NAME[VanillaLocations]: VanillaLocations.option_enabled,
|
||||
OPTION_NAME[ExtraLocations]: ExtraLocations.option_enabled,
|
||||
OPTION_NAME[ChallengeLocations]: ChallengeLocations.option_enabled,
|
||||
OPTION_NAME[MasteryLocations]: MasteryLocations.option_enabled,
|
||||
OPTION_NAME[WarCouncilNerfs]: WarCouncilNerfs.option_true,
|
||||
OPTION_NAME[NovaGhostOfAChanceVariant]: NovaGhostOfAChanceVariant.option_nco,
|
||||
OPTION_NAME[GenericUpgradeItems]: GenericUpgradeItems.option_individual_items,
|
||||
OPTION_NAME[MinNumberOfUpgrades]: 1,
|
||||
OPTION_NAME[MaxNumberOfUpgrades]: 2,
|
||||
OPTION_NAME[NovaMaxWeapons]: 1,
|
||||
OPTION_NAME[NovaMaxGadgets]: 1,
|
||||
OPTION_NAME[KerriganPresence]: KerriganPresence.option_not_present,
|
||||
OPTION_NAME[SpearOfAdunPresence]: SpearOfAdunPresence.option_any_race_lotv,
|
||||
OPTION_NAME[SpearOfAdunPresentInNoBuild]: SpearOfAdunPresentInNoBuild.option_false,
|
||||
OPTION_NAME[SpearOfAdunPassiveAbilityPresence]: SpearOfAdunPassiveAbilityPresence.option_any_race_lotv,
|
||||
OPTION_NAME[SpearOfAdunPassivesPresentInNoBuild]: SpearOfAdunPassivesPresentInNoBuild.option_false,
|
||||
OPTION_NAME[SpearOfAdunMaxActiveAbilities]: 2,
|
||||
OPTION_NAME[SpearOfAdunMaxAutocastAbilities]: 1,
|
||||
OPTION_NAME[MissionOrderScouting]: MissionOrderScouting.option_completed,
|
||||
OPTION_NAME[ExcludeOverpoweredItems]: ExcludeOverpoweredItems.option_true,
|
||||
OPTION_NAME[GrantStoryTech]: GrantStoryTech.option_allow_substitutes,
|
||||
OPTION_NAME[MineralsPerItem]: 10,
|
||||
OPTION_NAME[VespenePerItem]: 10,
|
||||
OPTION_NAME[StartingSupplyPerItem]: 2,
|
||||
OPTION_NAME[MaximumSupplyPerItem]: 1,
|
||||
OPTION_NAME[ResearchCostReductionPerItem]: 2,
|
||||
OPTION_NAME[MaximumSupplyReductionPerItem]: 10,
|
||||
OPTION_NAME[FillerPercentage]: 20,
|
||||
OPTION_NAME[FillerItemsDistribution]: {
|
||||
item_names.STARTING_MINERALS: 10,
|
||||
item_names.STARTING_VESPENE: 10,
|
||||
item_names.STARTING_SUPPLY: 10,
|
||||
item_names.MAX_SUPPLY: 0,
|
||||
item_names.SHIELD_REGENERATION: 5,
|
||||
item_names.BUILDING_CONSTRUCTION_SPEED: 10,
|
||||
item_names.KERRIGAN_LEVELS_1: 0,
|
||||
item_names.UPGRADE_RESEARCH_SPEED: 10,
|
||||
item_names.UPGRADE_RESEARCH_COST: 10,
|
||||
item_names.REDUCED_MAX_SUPPLY: 2,
|
||||
}
|
||||
}
|
||||
|
||||
full_campaign_settings = {
|
||||
# mandatory full campaign, not recommended, but will be expected. 195 mission grid, all races. Reduced locations and filler values
|
||||
OPTION_NAME[Accessibility]: Accessibility.option_full,
|
||||
OPTION_NAME[ProgressionBalancing]: ProgressionBalancing.default,
|
||||
OPTION_NAME[GameDifficulty]: GameDifficulty.option_normal,
|
||||
OPTION_NAME[SelectedRaces]: SelectedRaces.valid_keys,
|
||||
OPTION_NAME[MissionOrder]: MissionOrder.option_grid,
|
||||
OPTION_NAME[RequiredTactics]: RequiredTactics.option_standard,
|
||||
OPTION_NAME[EnabledCampaigns]: EnabledCampaigns.valid_keys,
|
||||
OPTION_NAME[EnableRaceSwapVariants]: EnableRaceSwapVariants.option_shuffle_all,
|
||||
OPTION_NAME[KeyMode]: KeyMode.option_progressive_missions,
|
||||
OPTION_NAME[MaximumCampaignSize]: MaximumCampaignSize.range_end,
|
||||
OPTION_NAME[TwoStartPositions]: TwoStartPositions.option_true,
|
||||
OPTION_NAME[StarterUnit]: StarterUnit.option_off,
|
||||
OPTION_NAME[EnableMorphling]: EnableMorphling.option_true,
|
||||
OPTION_NAME[NovaGhostOfAChanceVariant]: NovaGhostOfAChanceVariant.option_nco,
|
||||
OPTION_NAME[GrantStoryTech]: GrantStoryTech.option_allow_substitutes,
|
||||
OPTION_NAME[TakeOverAIAllies]: TakeOverAIAllies.option_false,
|
||||
OPTION_NAME[DifficultyCurve]: DifficultyCurve.option_standard,
|
||||
OPTION_NAME[VanillaLocations]: VanillaLocations.option_enabled,
|
||||
OPTION_NAME[ExtraLocations]: ExtraLocations.option_half_chance,
|
||||
OPTION_NAME[ChallengeLocations]: ChallengeLocations.option_disabled,
|
||||
OPTION_NAME[MasteryLocations]: MasteryLocations.option_disabled,
|
||||
OPTION_NAME[KerriganPrimalStatus]: KerriganPrimalStatus.option_item,
|
||||
OPTION_NAME[WarCouncilNerfs]: WarCouncilNerfs.option_true,
|
||||
OPTION_NAME[MaxUpgradeLevel]: 5,
|
||||
OPTION_NAME[SpearOfAdunPresence]: SpearOfAdunPresence.option_everywhere,
|
||||
OPTION_NAME[SpearOfAdunPresentInNoBuild]: SpearOfAdunPresentInNoBuild.option_false,
|
||||
OPTION_NAME[SpearOfAdunPassiveAbilityPresence]: SpearOfAdunPassiveAbilityPresence.option_everywhere,
|
||||
OPTION_NAME[SpearOfAdunPassivesPresentInNoBuild]: SpearOfAdunPassivesPresentInNoBuild.option_false,
|
||||
OPTION_NAME[MissionOrderScouting]: MissionOrderScouting.option_completed,
|
||||
OPTION_NAME[MineralsPerItem]: 5,
|
||||
OPTION_NAME[VespenePerItem]: 5,
|
||||
OPTION_NAME[StartingSupplyPerItem]: 1,
|
||||
OPTION_NAME[MaximumSupplyPerItem]: 1,
|
||||
OPTION_NAME[ResearchCostReductionPerItem]: 1,
|
||||
OPTION_NAME[FillerItemsDistribution]: {
|
||||
item_names.STARTING_MINERALS: 10,
|
||||
item_names.STARTING_VESPENE: 10,
|
||||
item_names.STARTING_SUPPLY: 10,
|
||||
item_names.MAX_SUPPLY: 10,
|
||||
item_names.SHIELD_REGENERATION: 5,
|
||||
item_names.BUILDING_CONSTRUCTION_SPEED: 10,
|
||||
item_names.KERRIGAN_LEVELS_1: 5,
|
||||
item_names.UPGRADE_RESEARCH_SPEED: 10,
|
||||
item_names.UPGRADE_RESEARCH_COST: 10,
|
||||
item_names.REDUCED_MAX_SUPPLY: 0,
|
||||
}
|
||||
}
|
||||
|
||||
sc2_options_presets: Dict[str, Dict[str, Any]] = {
|
||||
"F2P Terran [~7 hours]": template_settings,
|
||||
"F2P Big [~10 hours]": f2p_big_settings,
|
||||
"Zerg Rush [~3 hours]": zerg_rush_settings,
|
||||
"Classic Grid [~4 hours]": classic_grid_settings,
|
||||
"Raceswap Blitz [~8 hours]": raceswap_blitz_settings,
|
||||
"Bread and Butter [~12 hours]": bread_and_butter_settings,
|
||||
"Pure Protoss [~15 hours]": all_protoss_settings,
|
||||
"Evil Logic [~6 hours]": evil_logic_settings,
|
||||
"Giant Grid Game [30+ hours]": full_campaign_settings,
|
||||
}
|
||||
@@ -6,7 +6,8 @@ from datetime import timedelta
|
||||
from Options import (
|
||||
Choice, Toggle, DefaultOnToggle, OptionSet, Range,
|
||||
PerGameCommonOptions, Option, VerifyKeys, StartInventory,
|
||||
is_iterable_except_str, OptionGroup, Visibility, ItemDict
|
||||
is_iterable_except_str, OptionGroup, Visibility, ItemDict,
|
||||
Accessibility, ProgressionBalancing
|
||||
)
|
||||
from Utils import get_fuzzy_results
|
||||
from BaseClasses import PlandoOptions
|
||||
@@ -1756,3 +1757,6 @@ void_trade_age_limits_ms: Dict[int, int] = {
|
||||
VoidTradeAgeLimit.option_1_day: 1000 * int(timedelta(days = 1).total_seconds()),
|
||||
VoidTradeAgeLimit.option_1_week: 1000 * int(timedelta(weeks = 1).total_seconds()),
|
||||
}
|
||||
|
||||
# Store the names of all options
|
||||
OPTION_NAME = {option_type: name for name, option_type in Starcraft2Options.type_hints.items()}
|
||||
|
||||
@@ -13,8 +13,8 @@
|
||||
|
||||
## Installation
|
||||
|
||||
Open the [Randomizer Repository](https://github.com/BrandenEK/AShortHike.Randomizer) and follow
|
||||
the installation instructions listed there.
|
||||
1. Read the [Randomizer readme](https://github.com/BrandenEK/AShortHike.Randomizer) to see all required dependencies
|
||||
1. Read the [Mod Installer readme](https://github.com/BrandenEK/AShortHike.Modding.Installer) to see how to download the required mods
|
||||
|
||||
## Connecting
|
||||
|
||||
|
||||
28
worlds/smw/LICENSE
Normal file
28
worlds/smw/LICENSE
Normal file
@@ -0,0 +1,28 @@
|
||||
Modified MIT License
|
||||
|
||||
Copyright (c) 2025 PoryGone
|
||||
Copyright (c) 2025 lx5
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, and/or distribute copies of the Software,
|
||||
and to permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
No copy or substantial portion of the Software shall be sublicensed or relicensed
|
||||
without the express written permission of the copyright holder(s)
|
||||
|
||||
No copy or substantial portion of the Software shall be sold without the express
|
||||
written permission of the copyright holder(s)
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
6
worlds/smw/archipelago.json
Normal file
6
worlds/smw/archipelago.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"game": "Super Mario World",
|
||||
"authors": [ "PoryGone", "lx5" ],
|
||||
"minimum_ap_version": "0.6.3",
|
||||
"world_version": "2.1.1"
|
||||
}
|
||||
@@ -424,7 +424,6 @@ class Item:
|
||||
]
|
||||
|
||||
for item in itemPool:
|
||||
item.Progression = True
|
||||
item.World = world
|
||||
|
||||
return itemPool
|
||||
@@ -439,7 +438,6 @@ class Item:
|
||||
]
|
||||
|
||||
for item in itemPool:
|
||||
item.Progression = True
|
||||
item.World = world
|
||||
|
||||
return itemPool
|
||||
|
||||
@@ -145,8 +145,12 @@ class GanonsTower(Z3Region):
|
||||
|
||||
def CanFill(self, item: Item):
|
||||
if (self.Config.Multiworld):
|
||||
# changed for AP becuase upstream only uses CanFill for filling progression-related items
|
||||
# note that item.Progression does not include all items with progression classification
|
||||
# item.World will be None for item created by create_item for item links
|
||||
if (item.World is not None and (item.World != self.world or item.Progression)):
|
||||
if (item.World is not None and item.World != self.world and (item.Progression or item.IsDungeonItem() or item.IsKeycard() or item.IsSmMap())):
|
||||
return False
|
||||
if (item.World is not None and item.World == self.world and item.Progression):
|
||||
return False
|
||||
if (self.Config.Keysanity and not ((item.Type == ItemType.BigKeyGT or item.Type == ItemType.KeyGT) and item.World == self.world) and (item.IsKey() or item.IsBigKey() or item.IsKeycard())):
|
||||
return False
|
||||
|
||||
@@ -260,13 +260,19 @@ class SMZ3World(World):
|
||||
l.always_allow = lambda state, item, loc=loc: \
|
||||
item.game == "SMZ3" and \
|
||||
loc.alwaysAllow(item.item, state.smz3state[self.player])
|
||||
old_rule = l.item_rule
|
||||
l.item_rule = lambda item, loc=loc, region=region: (\
|
||||
l.item_rule = lambda item, loc=loc, region=region, old_rule=l.item_rule: (\
|
||||
item.game != "SMZ3" or \
|
||||
loc.allow(item.item, None) and \
|
||||
region.CanFill(item.item)) and old_rule(item)
|
||||
set_rule(l, lambda state, loc=loc: loc.Available(state.smz3state[self.player]))
|
||||
|
||||
# In multiworlds, GT is disallowed from having progression items.
|
||||
# This item rule replicates this behavior for non-SMZ3 games
|
||||
for loc in self.smz3World.GetRegion("Ganon's Tower").Locations:
|
||||
l = self.locations[loc.Name]
|
||||
l.item_rule = lambda item, old_rule=l.item_rule: \
|
||||
(item.game == "SMZ3" or not item.advancement) and old_rule(item)
|
||||
|
||||
def create_regions(self):
|
||||
self.create_locations(self.player)
|
||||
startRegion = self.create_region(self.multiworld, self.player, 'Menu')
|
||||
@@ -589,29 +595,18 @@ class SMZ3World(World):
|
||||
]))
|
||||
|
||||
def JunkFillGT(self, factor):
|
||||
poolLength = len(self.multiworld.itempool)
|
||||
junkPoolIdx = [i for i in range(0, poolLength)
|
||||
if self.multiworld.itempool[i].classification in (ItemClassification.filler, ItemClassification.trap)]
|
||||
junkPoolIdx = [idx for idx, i in enumerate(self.multiworld.itempool) if i.excludable]
|
||||
self.random.shuffle(junkPoolIdx)
|
||||
junkLocations = [loc for loc in self.locations.values() if loc.name in self.locationNamesGT and loc.item is None]
|
||||
self.random.shuffle(junkLocations)
|
||||
toRemove = []
|
||||
for loc in self.locations.values():
|
||||
# commenting this for now since doing a partial GT pre fill would allow for non SMZ3 progression in GT
|
||||
# which isnt desirable (SMZ3 logic only filters for SMZ3 items). Having progression in GT can only happen in Single Player.
|
||||
# if len(toRemove) >= int(len(self.locationNamesGT) * factor * self.smz3World.TowerCrystals / 7):
|
||||
# break
|
||||
if loc.name in self.locationNamesGT and loc.item is None:
|
||||
poolLength = len(junkPoolIdx)
|
||||
# start looking at a random starting index and loop at start if no match found
|
||||
start = self.multiworld.random.randint(0, poolLength)
|
||||
itemFromPool = None
|
||||
for off in range(0, poolLength):
|
||||
i = (start + off) % poolLength
|
||||
candidate = self.multiworld.itempool[junkPoolIdx[i]]
|
||||
if junkPoolIdx[i] not in toRemove and loc.can_fill(self.multiworld.state, candidate, False):
|
||||
itemFromPool = candidate
|
||||
toRemove.append(junkPoolIdx[i])
|
||||
break
|
||||
assert itemFromPool is not None, "Can't find anymore item(s) to pre fill GT"
|
||||
self.multiworld.push_item(loc, itemFromPool, False)
|
||||
for loc in junkLocations:
|
||||
# Note: Upstream GT junk fill uses FastFill, which ignores item rules
|
||||
if len(junkPoolIdx) == 0 or len(toRemove) >= int(len(junkLocations) * factor * self.smz3World.TowerCrystals / 7):
|
||||
break
|
||||
itemFromPool = self.multiworld.itempool[junkPoolIdx[0]]
|
||||
toRemove.append(junkPoolIdx.pop(0))
|
||||
loc.place_locked_item(itemFromPool)
|
||||
toRemove.sort(reverse = True)
|
||||
for i in toRemove:
|
||||
self.multiworld.itempool.pop(i)
|
||||
@@ -622,15 +617,15 @@ class SMZ3World(World):
|
||||
raise Exception(f"Tried to place item {itemType} at {location.Name}, but there is no such item in the item pool")
|
||||
else:
|
||||
location.Item = itemToPlace
|
||||
itemFromPool = next((i for i in self.multiworld.itempool if i.player == self.player and i.name == itemToPlace.Type.name), None)
|
||||
if itemFromPool is not None:
|
||||
itemPoolIdx = next((idx for idx, i in enumerate(self.multiworld.itempool) if i.player == self.player and i.name == itemToPlace.Type.name), None)
|
||||
if itemPoolIdx is not None:
|
||||
itemFromPool = self.multiworld.itempool.pop(itemPoolIdx)
|
||||
self.multiworld.get_location(location.Name, self.player).place_locked_item(itemFromPool)
|
||||
self.multiworld.itempool.remove(itemFromPool)
|
||||
else:
|
||||
itemFromPool = next((i for i in self.smz3DungeonItems if i.player == self.player and i.name == itemToPlace.Type.name), None)
|
||||
if itemFromPool is not None:
|
||||
itemPoolIdx = next((idx for idx, i in enumerate(self.smz3DungeonItems) if i.player == self.player and i.name == itemToPlace.Type.name), None)
|
||||
if itemPoolIdx is not None:
|
||||
itemFromPool = self.smz3DungeonItems.pop(itemPoolIdx)
|
||||
self.multiworld.get_location(location.Name, self.player).place_locked_item(itemFromPool)
|
||||
self.smz3DungeonItems.remove(itemFromPool)
|
||||
itemPool.remove(itemToPlace)
|
||||
|
||||
def FrontFillItemInOwnWorld(self, itemPool, itemType):
|
||||
@@ -640,10 +635,10 @@ class SMZ3World(World):
|
||||
raise Exception(f"Tried to front fill {item.Name} in, but no location was available")
|
||||
|
||||
location.Item = item
|
||||
itemFromPool = next((i for i in self.multiworld.itempool if i.player == self.player and i.name == item.Type.name and i.advancement == item.Progression), None)
|
||||
if itemFromPool is not None:
|
||||
itemPoolIdx = next((idx for idx, i in enumerate(self.multiworld.itempool) if i.player == self.player and i.name == item.Type.name and i.advancement == item.Progression), None)
|
||||
if itemPoolIdx is not None:
|
||||
itemFromPool = self.multiworld.itempool.pop(itemPoolIdx)
|
||||
self.multiworld.get_location(location.Name, self.player).place_locked_item(itemFromPool)
|
||||
self.multiworld.itempool.remove(itemFromPool)
|
||||
itemPool.remove(item)
|
||||
|
||||
def InitialFillInOwnWorld(self):
|
||||
|
||||
@@ -50,7 +50,6 @@ on the Archipelago website to generate a YAML using a graphical interface.
|
||||
significantly more difficult with this mod, so it is recommended to choose a lower difficulty than you normally would
|
||||
play on.
|
||||
4. Open the world in single player or multiplayer.
|
||||
5. When you're ready, open chat, and enter `/apstart` to start the game.
|
||||
|
||||
## Commands
|
||||
|
||||
|
||||
Reference in New Issue
Block a user