mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-03-11 01:53:46 -07:00
Compare commits
16 Commits
NewSoupVi-
...
NewSoupVi-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e7632670a6 | ||
|
|
65cf8080b1 | ||
|
|
45eef7d097 | ||
|
|
fc404d0cf7 | ||
|
|
5ce71db048 | ||
|
|
aff98a5b78 | ||
|
|
30cedb13f3 | ||
|
|
0c1ecf7297 | ||
|
|
5390561b58 | ||
|
|
bb457b0f73 | ||
|
|
6276ccf415 | ||
|
|
d3588a057c | ||
|
|
30ce74d6d5 | ||
|
|
ff59b86335 | ||
|
|
26c1e9b8c3 | ||
|
|
6bca1cbdac |
@@ -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 ""
|
||||
|
||||
39
Fill.py
39
Fill.py
@@ -210,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
|
||||
|
||||
@@ -135,6 +135,7 @@ def get_saving_second(seed_name: str, interval: int = 60) -> int:
|
||||
|
||||
class Client(Endpoint):
|
||||
__slots__ = (
|
||||
"__weakref__",
|
||||
"version",
|
||||
"auth",
|
||||
"team",
|
||||
@@ -216,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.
|
||||
@@ -245,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 = {}
|
||||
@@ -279,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[
|
||||
@@ -664,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}
|
||||
|
||||
}
|
||||
@@ -698,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"]
|
||||
|
||||
@@ -1529,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":
|
||||
@@ -2489,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 [])
|
||||
@@ -2576,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)
|
||||
@@ -2641,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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)),
|
||||
}
|
||||
|
||||
@@ -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: "";
|
||||
|
||||
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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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