mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-04-04 06:13:40 -07:00
Compare commits
33 Commits
0.6.5-rc1
...
webhost_ro
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a79f71ad8b | ||
|
|
ac84b272c5 | ||
|
|
e8a63abfa4 | ||
|
|
3fa2745c37 | ||
|
|
775065715d | ||
|
|
4e608b13ae | ||
|
|
886cc68051 | ||
|
|
146a314d22 | ||
|
|
18cf1bce36 | ||
|
|
f7e3f4e589 | ||
|
|
9f9765b78d | ||
|
|
8ae1a7da32 | ||
|
|
08ea3fe225 | ||
|
|
b81be6b4fc | ||
|
|
f1aca0fc46 | ||
|
|
d88fe99780 | ||
|
|
360a1384f2 | ||
|
|
d089b00ad5 | ||
|
|
c05a2adc38 | ||
|
|
7631242621 | ||
|
|
df48c3e718 | ||
|
|
9a755e64b2 | ||
|
|
34d362a003 | ||
|
|
b75cce5d41 | ||
|
|
a07faca2d9 | ||
|
|
8a1a715dc4 | ||
|
|
60a192b1b6 | ||
|
|
3b721e0365 | ||
|
|
3e16c20fce | ||
|
|
ec2c39e82f | ||
|
|
23d319247f | ||
|
|
c2c488410f | ||
|
|
8ea49e76db |
8
.github/workflows/build.yml
vendored
8
.github/workflows/build.yml
vendored
@@ -24,10 +24,10 @@ env:
|
|||||||
# NOTE: since appimage/appimagetool and appimage/type2-runtime does not have tags anymore,
|
# NOTE: since appimage/appimagetool and appimage/type2-runtime does not have tags anymore,
|
||||||
# we check the sha256 and require manual intervention if it was updated.
|
# we check the sha256 and require manual intervention if it was updated.
|
||||||
APPIMAGE_FORK: 'PopTracker'
|
APPIMAGE_FORK: 'PopTracker'
|
||||||
APPIMAGETOOL_VERSION: 'r-2025-10-19'
|
APPIMAGETOOL_VERSION: 'r-2025-11-18'
|
||||||
APPIMAGETOOL_X86_64_HASH: '9493a6b253a01f84acb9c624c38810ecfa11d99daa829b952b0bff43113080f9'
|
APPIMAGETOOL_X86_64_HASH: '4577a452b30af2337123fbb383aea154b618e51ad5448c3b62085cbbbfbfd9a2'
|
||||||
APPIMAGE_RUNTIME_VERSION: 'r-2025-08-11'
|
APPIMAGE_RUNTIME_VERSION: 'r-2025-11-07'
|
||||||
APPIMAGE_RUNTIME_X86_64_HASH: 'e70ffa9b69b211574d0917adc482dd66f25a0083427b5945783965d55b0b0a8b'
|
APPIMAGE_RUNTIME_X86_64_HASH: '27ddd3f78e483fc5f7856e413d7c17092917f8c35bfe3318a0d378aa9435ad17'
|
||||||
|
|
||||||
permissions: # permissions required for attestation
|
permissions: # permissions required for attestation
|
||||||
id-token: 'write'
|
id-token: 'write'
|
||||||
|
|||||||
8
.github/workflows/release.yml
vendored
8
.github/workflows/release.yml
vendored
@@ -12,10 +12,10 @@ env:
|
|||||||
# NOTE: since appimage/appimagetool and appimage/type2-runtime does not have tags anymore,
|
# NOTE: since appimage/appimagetool and appimage/type2-runtime does not have tags anymore,
|
||||||
# we check the sha256 and require manual intervention if it was updated.
|
# we check the sha256 and require manual intervention if it was updated.
|
||||||
APPIMAGE_FORK: 'PopTracker'
|
APPIMAGE_FORK: 'PopTracker'
|
||||||
APPIMAGETOOL_VERSION: 'r-2025-10-19'
|
APPIMAGETOOL_VERSION: 'r-2025-11-18'
|
||||||
APPIMAGETOOL_X86_64_HASH: '9493a6b253a01f84acb9c624c38810ecfa11d99daa829b952b0bff43113080f9'
|
APPIMAGETOOL_X86_64_HASH: '4577a452b30af2337123fbb383aea154b618e51ad5448c3b62085cbbbfbfd9a2'
|
||||||
APPIMAGE_RUNTIME_VERSION: 'r-2025-08-11'
|
APPIMAGE_RUNTIME_VERSION: 'r-2025-11-07'
|
||||||
APPIMAGE_RUNTIME_X86_64_HASH: 'e70ffa9b69b211574d0917adc482dd66f25a0083427b5945783965d55b0b0a8b'
|
APPIMAGE_RUNTIME_X86_64_HASH: '27ddd3f78e483fc5f7856e413d7c17092917f8c35bfe3318a0d378aa9435ad17'
|
||||||
|
|
||||||
permissions: # permissions required for attestation
|
permissions: # permissions required for attestation
|
||||||
id-token: 'write'
|
id-token: 'write'
|
||||||
|
|||||||
@@ -12,8 +12,8 @@
|
|||||||
<option name="IS_MODULE_SDK" value="true" />
|
<option name="IS_MODULE_SDK" value="true" />
|
||||||
<option name="ADD_CONTENT_ROOTS" value="true" />
|
<option name="ADD_CONTENT_ROOTS" value="true" />
|
||||||
<option name="ADD_SOURCE_ROOTS" value="true" />
|
<option name="ADD_SOURCE_ROOTS" value="true" />
|
||||||
<option name="SCRIPT_NAME" value="$ContentRoot$/Launcher.py" />
|
<option name="SCRIPT_NAME" value="$PROJECT_DIR$/Launcher.py" />
|
||||||
<option name="PARAMETERS" value="\"Build APWorlds\"" />
|
<option name="PARAMETERS" value=""Build APWorlds"" />
|
||||||
<option name="SHOW_COMMAND_LINE" value="false" />
|
<option name="SHOW_COMMAND_LINE" value="false" />
|
||||||
<option name="EMULATE_TERMINAL" value="false" />
|
<option name="EMULATE_TERMINAL" value="false" />
|
||||||
<option name="MODULE_MODE" value="false" />
|
<option name="MODULE_MODE" value="false" />
|
||||||
|
|||||||
@@ -323,7 +323,7 @@ class CommonContext:
|
|||||||
hint_cost: int | None
|
hint_cost: int | None
|
||||||
"""Current Hint Cost per Hint from the server"""
|
"""Current Hint Cost per Hint from the server"""
|
||||||
hint_points: int | None
|
hint_points: int | None
|
||||||
"""Current avaliable Hint Points from the server"""
|
"""Current available Hint Points from the server"""
|
||||||
player_names: dict[int, str]
|
player_names: dict[int, str]
|
||||||
"""Current lookup of slot number to player display name from server (includes aliases)"""
|
"""Current lookup of slot number to player display name from server (includes aliases)"""
|
||||||
|
|
||||||
|
|||||||
@@ -347,7 +347,9 @@ def update_weights(weights: dict, new_weights: dict, update_type: str, name: str
|
|||||||
elif isinstance(new_value, list):
|
elif isinstance(new_value, list):
|
||||||
cleaned_value.extend(new_value)
|
cleaned_value.extend(new_value)
|
||||||
elif isinstance(new_value, dict):
|
elif isinstance(new_value, dict):
|
||||||
cleaned_value = dict(Counter(cleaned_value) + Counter(new_value))
|
counter_value = Counter(cleaned_value)
|
||||||
|
counter_value.update(new_value)
|
||||||
|
cleaned_value = dict(counter_value)
|
||||||
else:
|
else:
|
||||||
raise Exception(f"Cannot apply merge to non-dict, set, or list type {option_name},"
|
raise Exception(f"Cannot apply merge to non-dict, set, or list type {option_name},"
|
||||||
f" received {type(new_value).__name__}.")
|
f" received {type(new_value).__name__}.")
|
||||||
@@ -361,7 +363,9 @@ def update_weights(weights: dict, new_weights: dict, update_type: str, name: str
|
|||||||
for element in new_value:
|
for element in new_value:
|
||||||
cleaned_value.remove(element)
|
cleaned_value.remove(element)
|
||||||
elif isinstance(new_value, dict):
|
elif isinstance(new_value, dict):
|
||||||
cleaned_value = dict(Counter(cleaned_value) - Counter(new_value))
|
counter_value = Counter(cleaned_value)
|
||||||
|
counter_value.subtract(new_value)
|
||||||
|
cleaned_value = dict(counter_value)
|
||||||
else:
|
else:
|
||||||
raise Exception(f"Cannot apply remove to non-dict, set, or list type {option_name},"
|
raise Exception(f"Cannot apply remove to non-dict, set, or list type {option_name},"
|
||||||
f" received {type(new_value).__name__}.")
|
f" received {type(new_value).__name__}.")
|
||||||
|
|||||||
@@ -493,7 +493,7 @@ class Context:
|
|||||||
self.read_data["race_mode"] = lambda: decoded_obj.get("race_mode", 0)
|
self.read_data["race_mode"] = lambda: decoded_obj.get("race_mode", 0)
|
||||||
mdata_ver = decoded_obj["minimum_versions"]["server"]
|
mdata_ver = decoded_obj["minimum_versions"]["server"]
|
||||||
if mdata_ver > version_tuple:
|
if mdata_ver > version_tuple:
|
||||||
raise RuntimeError(f"Supplied Multidata (.archipelago) requires a server of at least version {mdata_ver},"
|
raise RuntimeError(f"Supplied Multidata (.archipelago) requires a server of at least version {mdata_ver}, "
|
||||||
f"however this server is of version {version_tuple}")
|
f"however this server is of version {version_tuple}")
|
||||||
self.generator_version = Version(*decoded_obj["version"])
|
self.generator_version = Version(*decoded_obj["version"])
|
||||||
clients_ver = decoded_obj["minimum_versions"].get("clients", {})
|
clients_ver = decoded_obj["minimum_versions"].get("clients", {})
|
||||||
|
|||||||
@@ -1545,6 +1545,7 @@ class PlandoItems(Option[typing.List[PlandoItem]]):
|
|||||||
default = ()
|
default = ()
|
||||||
supports_weighting = False
|
supports_weighting = False
|
||||||
display_name = "Plando Items"
|
display_name = "Plando Items"
|
||||||
|
visibility = Visibility.template | Visibility.spoiler
|
||||||
|
|
||||||
def __init__(self, value: typing.Iterable[PlandoItem]) -> None:
|
def __init__(self, value: typing.Iterable[PlandoItem]) -> None:
|
||||||
self.value = list(deepcopy(value))
|
self.value = list(deepcopy(value))
|
||||||
|
|||||||
@@ -1,3 +1,9 @@
|
|||||||
|
if __name__ == "__main__":
|
||||||
|
import ModuleUpdate
|
||||||
|
|
||||||
|
ModuleUpdate.update()
|
||||||
|
|
||||||
|
|
||||||
from kvui import (ThemedApp, ScrollBox, MainLayout, ContainerLayout, dp, Widget, MDBoxLayout, TooltipLabel, MDLabel,
|
from kvui import (ThemedApp, ScrollBox, MainLayout, ContainerLayout, dp, Widget, MDBoxLayout, TooltipLabel, MDLabel,
|
||||||
ToggleButton, MarkupDropdown, ResizableTextField)
|
ToggleButton, MarkupDropdown, ResizableTextField)
|
||||||
from kivy.uix.behaviors.button import ButtonBehavior
|
from kivy.uix.behaviors.button import ButtonBehavior
|
||||||
@@ -330,6 +336,11 @@ class OptionsCreator(ThemedApp):
|
|||||||
box.range.slider.dropdown.open()
|
box.range.slider.dropdown.open()
|
||||||
|
|
||||||
box = VisualNamedRange(option=option, name=name, range_widget=self.create_range(option, name))
|
box = VisualNamedRange(option=option, name=name, range_widget=self.create_range(option, name))
|
||||||
|
if option.default in option.special_range_names:
|
||||||
|
# value can get mismatched in this case
|
||||||
|
box.range.slider.value = min(max(option.special_range_names[option.default], option.range_start),
|
||||||
|
option.range_end)
|
||||||
|
box.range.tag.text = str(int(box.range.slider.value))
|
||||||
box.range.slider.bind(on_touch_move=lambda _, _2: set_to_custom(box))
|
box.range.slider.bind(on_touch_move=lambda _, _2: set_to_custom(box))
|
||||||
items = [
|
items = [
|
||||||
{
|
{
|
||||||
@@ -365,7 +376,7 @@ class OptionsCreator(ThemedApp):
|
|||||||
# for some reason this fixes an issue causing some to not open
|
# for some reason this fixes an issue causing some to not open
|
||||||
dropdown.open()
|
dropdown.open()
|
||||||
|
|
||||||
default_random = option.default == "random"
|
default_string = isinstance(option.default, str)
|
||||||
main_button = VisualChoice(option=option, name=name)
|
main_button = VisualChoice(option=option, name=name)
|
||||||
main_button.bind(on_release=open_dropdown)
|
main_button.bind(on_release=open_dropdown)
|
||||||
|
|
||||||
@@ -377,7 +388,7 @@ class OptionsCreator(ThemedApp):
|
|||||||
for choice in option.name_lookup
|
for choice in option.name_lookup
|
||||||
]
|
]
|
||||||
dropdown = MDDropdownMenu(caller=main_button, items=items)
|
dropdown = MDDropdownMenu(caller=main_button, items=items)
|
||||||
self.options[name] = option.name_lookup[option.default] if not default_random else option.default
|
self.options[name] = option.name_lookup[option.default] if not default_string else option.default
|
||||||
return main_button
|
return main_button
|
||||||
|
|
||||||
def create_text_choice(self, option: typing.Type[TextChoice], name: str):
|
def create_text_choice(self, option: typing.Type[TextChoice], name: str):
|
||||||
@@ -560,8 +571,11 @@ class OptionsCreator(ThemedApp):
|
|||||||
groups[group].append((name, option))
|
groups[group].append((name, option))
|
||||||
|
|
||||||
for group, options in groups.items():
|
for group, options in groups.items():
|
||||||
|
options = [(name, option) for name, option in options
|
||||||
|
if name and option.visibility & Visibility.simple_ui]
|
||||||
if not options:
|
if not options:
|
||||||
continue # Game Options can be empty if every other option is in another group
|
continue # Game Options can be empty if every other option is in another group
|
||||||
|
# Can also have an option group of options that should not render on simple ui
|
||||||
group_item = MDExpansionPanel(size_hint_y=None)
|
group_item = MDExpansionPanel(size_hint_y=None)
|
||||||
group_header = MDExpansionPanelHeader(MDListItem(MDListItemSupportingText(text=group),
|
group_header = MDExpansionPanelHeader(MDListItem(MDListItemSupportingText(text=group),
|
||||||
TrailingPressedIconButton(icon="chevron-right",
|
TrailingPressedIconButton(icon="chevron-right",
|
||||||
@@ -583,8 +597,7 @@ class OptionsCreator(ThemedApp):
|
|||||||
group_box.layout.orientation = "vertical"
|
group_box.layout.orientation = "vertical"
|
||||||
group_box.layout.spacing = dp(3)
|
group_box.layout.spacing = dp(3)
|
||||||
for name, option in options:
|
for name, option in options:
|
||||||
if name and option is not Removed and option.visibility & Visibility.simple_ui:
|
group_content.add_widget(self.create_option(option, name, cls))
|
||||||
group_content.add_widget(self.create_option(option, name, cls))
|
|
||||||
expansion_box.layout.add_widget(group_item)
|
expansion_box.layout.add_widget(group_item)
|
||||||
self.option_layout.add_widget(expansion_box)
|
self.option_layout.add_widget(expansion_box)
|
||||||
self.game_label.text = f"Game: {self.current_game}"
|
self.game_label.text = f"Game: {self.current_game}"
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ app.jinja_env.filters['get_file_safe_name'] = get_file_safe_name
|
|||||||
app.config["SELFHOST"] = True # application process is in charge of running the websites
|
app.config["SELFHOST"] = True # application process is in charge of running the websites
|
||||||
app.config["GENERATORS"] = 8 # maximum concurrent world gens
|
app.config["GENERATORS"] = 8 # maximum concurrent world gens
|
||||||
app.config["HOSTERS"] = 8 # maximum concurrent room hosters
|
app.config["HOSTERS"] = 8 # maximum concurrent room hosters
|
||||||
|
app.config["ROOM_IDLE_TIMEOUT"] = 2 * 60 * 60 # seconds of idle before a Room spins down
|
||||||
app.config["SELFLAUNCH"] = True # application process is in charge of launching Rooms.
|
app.config["SELFLAUNCH"] = True # application process is in charge of launching Rooms.
|
||||||
app.config["SELFLAUNCHCERT"] = None # can point to a SSL Certificate to encrypt Room websocket connections
|
app.config["SELFLAUNCHCERT"] = None # can point to a SSL Certificate to encrypt Room websocket connections
|
||||||
app.config["SELFLAUNCHKEY"] = None # can point to a SSL Certificate Key to encrypt Room websocket connections
|
app.config["SELFLAUNCHKEY"] = None # can point to a SSL Certificate Key to encrypt Room websocket connections
|
||||||
|
|||||||
@@ -38,6 +38,5 @@ def room_info(room_id: UUID) -> Dict[str, Any]:
|
|||||||
"players": get_players(room.seed),
|
"players": get_players(room.seed),
|
||||||
"last_port": room.last_port,
|
"last_port": room.last_port,
|
||||||
"last_activity": room.last_activity,
|
"last_activity": room.last_activity,
|
||||||
"timeout": room.timeout,
|
|
||||||
"downloads": downloads,
|
"downloads": downloads,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ def get_rooms():
|
|||||||
"creation_time": room.creation_time,
|
"creation_time": room.creation_time,
|
||||||
"last_activity": room.last_activity,
|
"last_activity": room.last_activity,
|
||||||
"last_port": room.last_port,
|
"last_port": room.last_port,
|
||||||
"timeout": room.timeout,
|
|
||||||
"tracker": to_url(room.tracker),
|
"tracker": to_url(room.tracker),
|
||||||
})
|
})
|
||||||
return jsonify(response)
|
return jsonify(response)
|
||||||
|
|||||||
@@ -124,16 +124,14 @@ def autohost(config: dict):
|
|||||||
hoster = MultiworldInstance(config, x)
|
hoster = MultiworldInstance(config, x)
|
||||||
hosters.append(hoster)
|
hosters.append(hoster)
|
||||||
hoster.start()
|
hoster.start()
|
||||||
|
activity_timedelta = timedelta(seconds=config["ROOM_IDLE_TIMEOUT"] + 5)
|
||||||
while not stop_event.wait(0.1):
|
while not stop_event.wait(0.1):
|
||||||
with db_session:
|
with db_session:
|
||||||
rooms = select(
|
rooms = select(
|
||||||
room for room in Room if
|
room for room in Room if
|
||||||
room.last_activity >= datetime.utcnow() - timedelta(days=3))
|
room.last_activity >= datetime.utcnow() - activity_timedelta)
|
||||||
for room in rooms:
|
for room in rooms:
|
||||||
# we have to filter twice, as the per-room timeout can't currently be PonyORM transpiled.
|
hosters[room.id.int % len(hosters)].start_room(room.id)
|
||||||
if room.last_activity >= datetime.utcnow() - timedelta(seconds=room.timeout + 5):
|
|
||||||
hosters[room.id.int % len(hosters)].start_room(room.id)
|
|
||||||
|
|
||||||
except AlreadyRunningException:
|
except AlreadyRunningException:
|
||||||
logging.info("Autohost reports as already running, not starting another.")
|
logging.info("Autohost reports as already running, not starting another.")
|
||||||
@@ -187,6 +185,7 @@ class MultiworldInstance():
|
|||||||
self.cert = config["SELFLAUNCHCERT"]
|
self.cert = config["SELFLAUNCHCERT"]
|
||||||
self.key = config["SELFLAUNCHKEY"]
|
self.key = config["SELFLAUNCHKEY"]
|
||||||
self.host = config["HOST_ADDRESS"]
|
self.host = config["HOST_ADDRESS"]
|
||||||
|
self.room_idle_timer = config["ROOM_IDLE_TIMEOUT"]
|
||||||
self.rooms_to_start = multiprocessing.Queue()
|
self.rooms_to_start = multiprocessing.Queue()
|
||||||
self.rooms_shutting_down = multiprocessing.Queue()
|
self.rooms_shutting_down = multiprocessing.Queue()
|
||||||
self.name = f"MultiHoster{id}"
|
self.name = f"MultiHoster{id}"
|
||||||
@@ -198,7 +197,7 @@ class MultiworldInstance():
|
|||||||
process = multiprocessing.Process(group=None, target=run_server_process,
|
process = multiprocessing.Process(group=None, target=run_server_process,
|
||||||
args=(self.name, self.ponyconfig, get_static_server_data(),
|
args=(self.name, self.ponyconfig, get_static_server_data(),
|
||||||
self.cert, self.key, self.host,
|
self.cert, self.key, self.host,
|
||||||
self.rooms_to_start, self.rooms_shutting_down),
|
self.rooms_to_start, self.rooms_shutting_down, self.room_idle_timer),
|
||||||
name=self.name)
|
name=self.name)
|
||||||
process.start()
|
process.start()
|
||||||
self.process = process
|
self.process = process
|
||||||
|
|||||||
@@ -231,7 +231,8 @@ def set_up_logging(room_id) -> logging.Logger:
|
|||||||
|
|
||||||
def run_server_process(name: str, ponyconfig: dict, static_server_data: dict,
|
def run_server_process(name: str, ponyconfig: dict, static_server_data: dict,
|
||||||
cert_file: typing.Optional[str], cert_key_file: typing.Optional[str],
|
cert_file: typing.Optional[str], cert_key_file: typing.Optional[str],
|
||||||
host: str, rooms_to_run: multiprocessing.Queue, rooms_shutting_down: multiprocessing.Queue):
|
host: str, rooms_to_run: multiprocessing.Queue, rooms_shutting_down: multiprocessing.Queue,
|
||||||
|
room_idle_timeout: int):
|
||||||
from setproctitle import setproctitle
|
from setproctitle import setproctitle
|
||||||
|
|
||||||
setproctitle(name)
|
setproctitle(name)
|
||||||
@@ -316,7 +317,7 @@ def run_server_process(name: str, ponyconfig: dict, static_server_data: dict,
|
|||||||
else:
|
else:
|
||||||
ctx.logger.exception("Could not determine port. Likely hosting failure.")
|
ctx.logger.exception("Could not determine port. Likely hosting failure.")
|
||||||
with db_session:
|
with db_session:
|
||||||
ctx.auto_shutdown = Room.get(id=room_id).timeout
|
ctx.auto_shutdown = room_idle_timeout
|
||||||
if ctx.saving:
|
if ctx.saving:
|
||||||
setattr(asyncio.current_task(), "save", lambda: ctx._save(True))
|
setattr(asyncio.current_task(), "save", lambda: ctx._save(True))
|
||||||
assert ctx.shutdown_task is None
|
assert ctx.shutdown_task is None
|
||||||
@@ -347,7 +348,7 @@ def run_server_process(name: str, ponyconfig: dict, static_server_data: dict,
|
|||||||
# ensure the Room does not spin up again on its own, minute of safety buffer
|
# ensure the Room does not spin up again on its own, minute of safety buffer
|
||||||
room = Room.get(id=room_id)
|
room = Room.get(id=room_id)
|
||||||
room.last_activity = datetime.datetime.utcnow() - \
|
room.last_activity = datetime.datetime.utcnow() - \
|
||||||
datetime.timedelta(minutes=1, seconds=room.timeout)
|
datetime.timedelta(minutes=1, seconds=room_idle_timeout)
|
||||||
del room
|
del room
|
||||||
logging.info(f"Shutting down room {room_id} on {name}.")
|
logging.info(f"Shutting down room {room_id} on {name}.")
|
||||||
finally:
|
finally:
|
||||||
|
|||||||
@@ -231,7 +231,7 @@ def host_room(room: UUID):
|
|||||||
now = datetime.datetime.utcnow()
|
now = datetime.datetime.utcnow()
|
||||||
# indicate that the page should reload to get the assigned port
|
# indicate that the page should reload to get the assigned port
|
||||||
should_refresh = ((not room.last_port and now - room.creation_time < datetime.timedelta(seconds=3))
|
should_refresh = ((not room.last_port and now - room.creation_time < datetime.timedelta(seconds=3))
|
||||||
or room.last_activity < now - datetime.timedelta(seconds=room.timeout))
|
or room.last_activity < now - datetime.timedelta(seconds=app.config["ROOM_IDLE_TIMEOUT"]))
|
||||||
|
|
||||||
if now - room.last_activity > datetime.timedelta(minutes=1):
|
if now - room.last_activity > datetime.timedelta(minutes=1):
|
||||||
# we only set last_activity if needed, otherwise parallel access on /room will cause an internal server error
|
# we only set last_activity if needed, otherwise parallel access on /room will cause an internal server error
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ class Room(db.Entity):
|
|||||||
seed = Required('Seed', index=True)
|
seed = Required('Seed', index=True)
|
||||||
multisave = Optional(buffer, lazy=True)
|
multisave = Optional(buffer, lazy=True)
|
||||||
show_spoiler = Required(int, default=0) # 0 -> never, 1 -> after completion, -> 2 always
|
show_spoiler = Required(int, default=0) # 0 -> never, 1 -> after completion, -> 2 always
|
||||||
timeout = Required(int, default=lambda: 2 * 60 * 60) # seconds since last activity to shutdown
|
|
||||||
tracker = Optional(UUID, index=True)
|
tracker = Optional(UUID, index=True)
|
||||||
# Port special value -1 means the server errored out. Another attempt can be made with a page refresh
|
# Port special value -1 means the server errored out. Another attempt can be made with a page refresh
|
||||||
last_port = Optional(int, default=lambda: 0)
|
last_port = Optional(int, default=lambda: 0)
|
||||||
|
|||||||
@@ -29,7 +29,7 @@
|
|||||||
and a <a href="{{ url_for("get_multiworld_sphere_tracker", tracker=room.tracker) }}">Sphere Tracker</a> enabled.
|
and a <a href="{{ url_for("get_multiworld_sphere_tracker", tracker=room.tracker) }}">Sphere Tracker</a> enabled.
|
||||||
<br />
|
<br />
|
||||||
{% endif %}
|
{% endif %}
|
||||||
The server for this room will be paused after {{ room.timeout//60//60 }} hours of inactivity.
|
The server for this room will be paused after {{ config["ROOM_IDLE_TIMEOUT"]//60//60 }} hours of inactivity.
|
||||||
Should you wish to continue later,
|
Should you wish to continue later,
|
||||||
anyone can simply refresh this page and the server will resume.<br>
|
anyone can simply refresh this page and the server will resume.<br>
|
||||||
{% if room.last_port == -1 %}
|
{% if room.last_port == -1 %}
|
||||||
|
|||||||
@@ -959,7 +959,7 @@ if "Timespinner" in network_data_package["games"]:
|
|||||||
|
|
||||||
timespinner_location_ids = {
|
timespinner_location_ids = {
|
||||||
"Present": list(range(1337000, 1337085)),
|
"Present": list(range(1337000, 1337085)),
|
||||||
"Past": list(range(1337086, 1337175)),
|
"Past": list(range(1337086, 1337157)) + list(range(1337159, 1337175)),
|
||||||
"Ancient Pyramid": [
|
"Ancient Pyramid": [
|
||||||
1337236,
|
1337236,
|
||||||
1337246, 1337247, 1337248, 1337249]
|
1337246, 1337247, 1337248, 1337249]
|
||||||
|
|||||||
@@ -289,7 +289,7 @@ async def nes_sync_task(ctx: ZeldaContext):
|
|||||||
if not ctx.auth:
|
if not ctx.auth:
|
||||||
ctx.auth = ''.join([chr(i) for i in data_decoded['playerName'] if i != 0])
|
ctx.auth = ''.join([chr(i) for i in data_decoded['playerName'] if i != 0])
|
||||||
if ctx.auth == '':
|
if ctx.auth == '':
|
||||||
logger.info("Invalid ROM detected. No player name built into the ROM. Please regenerate"
|
logger.info("Invalid ROM detected. No player name built into the ROM. Please regenerate "
|
||||||
"the ROM using the same link but adding your slot name")
|
"the ROM using the same link but adding your slot name")
|
||||||
if ctx.awaiting_rom:
|
if ctx.awaiting_rom:
|
||||||
await ctx.server_auth(False)
|
await ctx.server_auth(False)
|
||||||
|
|||||||
@@ -6,12 +6,12 @@
|
|||||||
tag: tag
|
tag: tag
|
||||||
MDLabel:
|
MDLabel:
|
||||||
id: tag
|
id: tag
|
||||||
text: str(this.option.default) if this.option.default != "random" else this.option.range_start
|
text: str(this.option.default) if not isinstance(this.option.default, str) else str(this.option.range_start)
|
||||||
MDSlider:
|
MDSlider:
|
||||||
id: slider
|
id: slider
|
||||||
min: this.option.range_start
|
min: this.option.range_start
|
||||||
max: this.option.range_end
|
max: this.option.range_end
|
||||||
value: min(max(this.option.default, this.option.range_start), this.option.range_end) if this.option.default != "random" else this.option.range_start
|
value: min(max(this.option.default, this.option.range_start), this.option.range_end) if not isinstance(this.option.default, str) else this.option.range_start
|
||||||
step: 1
|
step: 1
|
||||||
step_point_size: 0
|
step_point_size: 0
|
||||||
MDSliderHandle:
|
MDSliderHandle:
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
text: text
|
text: text
|
||||||
MDButtonText:
|
MDButtonText:
|
||||||
id: text
|
id: text
|
||||||
text: this.option.get_option_name(this.option.default if this.option.default != "random" else list(this.option.options.values())[0])
|
text: this.option.get_option_name(this.option.default if not isinstance(this.option.default, str) else list(this.option.options.values())[0])
|
||||||
theme_text_color: "Primary"
|
theme_text_color: "Primary"
|
||||||
|
|
||||||
<VisualNamedRange>:
|
<VisualNamedRange>:
|
||||||
@@ -38,7 +38,7 @@
|
|||||||
text: text
|
text: text
|
||||||
MDButtonText:
|
MDButtonText:
|
||||||
id: text
|
id: text
|
||||||
text: this.option.special_range_names.get(list(this.option.special_range_names.values()).index(this.option.default)) if this.option.default in this.option.special_range_names else "Custom"
|
text: this.option.default.title() if this.option.default in this.option.special_range_names else "Custom"
|
||||||
|
|
||||||
<VisualFreeText>:
|
<VisualFreeText>:
|
||||||
multiline: False
|
multiline: False
|
||||||
|
|||||||
@@ -44,9 +44,9 @@ These get automatically added to the `archipelago.json` of an .apworld if it is
|
|||||||
["Build apworlds" launcher component](#build-apworlds-launcher-component),
|
["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.
|
which is the correct way to package your `.apworld` as a world developer. Do not write these fields yourself.
|
||||||
|
|
||||||
### "Build apworlds" Launcher Component
|
### "Build APWorlds" Launcher Component
|
||||||
|
|
||||||
In the Archipelago Launcher, there is a "Build apworlds" component that will package all world folders to `.apworld`,
|
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.
|
and add `archipelago.json` manifest files to them.
|
||||||
These .apworld files will be output to `build/apworlds` (relative to the Archipelago root directory).
|
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
|
The `archipelago.json` file in each .apworld will automatically include the appropriate
|
||||||
|
|||||||
@@ -269,7 +269,8 @@ placed on them.
|
|||||||
|
|
||||||
### PriorityLocations
|
### PriorityLocations
|
||||||
Marks locations given here as `LocationProgressType.Priority` forcing progression items on them if any are available in
|
Marks locations given here as `LocationProgressType.Priority` forcing progression items on them if any are available in
|
||||||
the pool.
|
the pool. Progression items without a deprioritized flag will be used first when filling priority_locations. Progression items with
|
||||||
|
a deprioritized flag will be used next.
|
||||||
|
|
||||||
### ItemLinks
|
### ItemLinks
|
||||||
Allows users to share their item pool with other players. Currently item links are per game. A link of one game between
|
Allows users to share their item pool with other players. Currently item links are per game. A link of one game between
|
||||||
|
|||||||
@@ -525,7 +525,7 @@ def randomize_entrances(
|
|||||||
|
|
||||||
running_time = time.perf_counter() - start_time
|
running_time = time.perf_counter() - start_time
|
||||||
if running_time > 1.0:
|
if running_time > 1.0:
|
||||||
logging.info(f"Took {running_time:.4f} seconds during entrance randomization for player {world.player},"
|
logging.info(f"Took {running_time:.4f} seconds during entrance randomization for player {world.player}, "
|
||||||
f"named {world.multiworld.player_name[world.player]}")
|
f"named {world.multiworld.player_name[world.player]}")
|
||||||
|
|
||||||
return er_state
|
return er_state
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
colorama>=0.4.6
|
colorama>=0.4.6
|
||||||
websockets>=13.0.1,<14
|
websockets>=13.0.1,<14
|
||||||
PyYAML>=6.0.2
|
PyYAML>=6.0.3
|
||||||
jellyfish>=1.1.3
|
jellyfish>=1.2.1
|
||||||
jinja2>=3.1.6
|
jinja2>=3.1.6
|
||||||
schema>=0.7.7
|
schema>=0.7.8
|
||||||
kivy>=2.3.1
|
kivy>=2.3.1
|
||||||
bsdiff4>=1.2.6
|
bsdiff4>=1.2.6
|
||||||
platformdirs>=4.3.6
|
platformdirs>=4.5.0
|
||||||
certifi>=2025.4.26
|
certifi>=2025.11.12
|
||||||
cython>=3.0.12
|
cython>=3.2.1
|
||||||
cymem>=2.0.11
|
cymem>=2.0.13
|
||||||
orjson>=3.10.15
|
orjson>=3.11.4
|
||||||
typing_extensions>=4.12.2
|
typing_extensions>=4.15.0
|
||||||
pyshortcuts>=1.9.1
|
pyshortcuts>=1.9.6
|
||||||
kivymd @ git+https://github.com/kivymd/KivyMD@5ff9d0d
|
kivymd @ git+https://github.com/kivymd/KivyMD@5ff9d0d
|
||||||
kivymd>=2.0.1.dev0
|
kivymd>=2.0.1.dev0
|
||||||
|
|||||||
4
setup.py
4
setup.py
@@ -394,11 +394,11 @@ class BuildExeCommand(cx_Freeze.command.build_exe.build_exe):
|
|||||||
manifest = json.load(manifest_file)
|
manifest = json.load(manifest_file)
|
||||||
|
|
||||||
assert "game" in manifest, (
|
assert "game" in manifest, (
|
||||||
f"World directory {world_directory} has an archipelago.json manifest file, but it"
|
f"World directory {world_directory} has an archipelago.json manifest file, but it "
|
||||||
"does not define a \"game\"."
|
"does not define a \"game\"."
|
||||||
)
|
)
|
||||||
assert manifest["game"] == worldtype.game, (
|
assert manifest["game"] == worldtype.game, (
|
||||||
f"World directory {world_directory} has an archipelago.json manifest file, but value of the"
|
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})."
|
f"\"game\" field ({manifest['game']} does not equal the World class's game ({worldtype.game})."
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -37,3 +37,23 @@ class TestPlayerOptions(unittest.TestCase):
|
|||||||
self.assertEqual(new_weights["dict_2"]["option_g"], 50)
|
self.assertEqual(new_weights["dict_2"]["option_g"], 50)
|
||||||
self.assertEqual(len(new_weights["set_1"]), 2)
|
self.assertEqual(len(new_weights["set_1"]), 2)
|
||||||
self.assertIn("option_d", new_weights["set_1"])
|
self.assertIn("option_d", new_weights["set_1"])
|
||||||
|
|
||||||
|
def test_update_dict_supports_negatives_and_zeroes(self):
|
||||||
|
original_options = {
|
||||||
|
"dict_1": {"a": 1, "b": -1},
|
||||||
|
"dict_2": {"a": 1, "b": -1},
|
||||||
|
}
|
||||||
|
new_weights = Generate.update_weights(
|
||||||
|
original_options,
|
||||||
|
{
|
||||||
|
"+dict_1": {"a": -2, "b": 2},
|
||||||
|
"-dict_2": {"a": 1, "b": 2},
|
||||||
|
},
|
||||||
|
"Tested",
|
||||||
|
"",
|
||||||
|
)
|
||||||
|
self.assertEqual(new_weights["dict_1"]["a"], -1)
|
||||||
|
self.assertEqual(new_weights["dict_1"]["b"], 1)
|
||||||
|
self.assertEqual(new_weights["dict_2"]["a"], 0)
|
||||||
|
self.assertEqual(new_weights["dict_2"]["b"], -3)
|
||||||
|
self.assertIn("a", new_weights["dict_2"])
|
||||||
|
|||||||
@@ -70,13 +70,13 @@ if __name__ == "__main__":
|
|||||||
empty_file = str(Path(tempdir) / "empty")
|
empty_file = str(Path(tempdir) / "empty")
|
||||||
open(empty_file, "w").close()
|
open(empty_file, "w").close()
|
||||||
sys.argv += ["--config_override", empty_file] # tests #5541
|
sys.argv += ["--config_override", empty_file] # tests #5541
|
||||||
multis = [["VVVVVV"], ["Temp World"], ["VVVVVV", "Temp World"]]
|
multis = [["APQuest"], ["Temp World"], ["APQuest", "Temp World"]]
|
||||||
p1_games: list[str] = []
|
p1_games: list[str] = []
|
||||||
data_paths: list[Path | None] = []
|
data_paths: list[Path | None] = []
|
||||||
rooms: list[str] = []
|
rooms: list[str] = []
|
||||||
multidata: Path | None
|
multidata: Path | None
|
||||||
|
|
||||||
copy_world("VVVVVV", "Temp World")
|
copy_world("APQuest", "Temp World")
|
||||||
try:
|
try:
|
||||||
for n, games in enumerate(multis, 1):
|
for n, games in enumerate(multis, 1):
|
||||||
print(f"Generating [{n}] {', '.join(games)} offline")
|
print(f"Generating [{n}] {', '.join(games)} offline")
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ __all__ = [
|
|||||||
"create_room",
|
"create_room",
|
||||||
"start_room",
|
"start_room",
|
||||||
"stop_room",
|
"stop_room",
|
||||||
"set_room_timeout",
|
|
||||||
"get_multidata_for_room",
|
"get_multidata_for_room",
|
||||||
"set_multidata_for_room",
|
"set_multidata_for_room",
|
||||||
"stop_autogen",
|
"stop_autogen",
|
||||||
@@ -139,7 +138,6 @@ def stop_room(app_client: "FlaskClient",
|
|||||||
from pony.orm import db_session
|
from pony.orm import db_session
|
||||||
|
|
||||||
from WebHostLib.models import Command, Room
|
from WebHostLib.models import Command, Room
|
||||||
from WebHostLib import app
|
|
||||||
|
|
||||||
poll_interval = 2
|
poll_interval = 2
|
||||||
|
|
||||||
@@ -152,14 +150,12 @@ def stop_room(app_client: "FlaskClient",
|
|||||||
with db_session:
|
with db_session:
|
||||||
room: Room = Room.get(id=room_uuid)
|
room: Room = Room.get(id=room_uuid)
|
||||||
if simulate_idle:
|
if simulate_idle:
|
||||||
new_last_activity = datetime.utcnow() - timedelta(seconds=room.timeout + 5)
|
new_last_activity = datetime.utcnow() - timedelta(seconds=2 * 60 * 60 + 5)
|
||||||
else:
|
else:
|
||||||
new_last_activity = datetime.utcnow() - timedelta(days=3)
|
new_last_activity = datetime.utcnow() - timedelta(days=3)
|
||||||
room.last_activity = new_last_activity
|
room.last_activity = new_last_activity
|
||||||
address = f"localhost:{room.last_port}" if room.last_port > 0 else None
|
address = f"localhost:{room.last_port}" if room.last_port > 0 else None
|
||||||
if address:
|
if address:
|
||||||
original_timeout = room.timeout
|
|
||||||
room.timeout = 1 # avoid spinning it up again
|
|
||||||
Command(room=room, commandtext="/exit")
|
Command(room=room, commandtext="/exit")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -186,28 +182,13 @@ def stop_room(app_client: "FlaskClient",
|
|||||||
room = Room.get(id=room_uuid)
|
room = Room.get(id=room_uuid)
|
||||||
room.last_port = 0 # easier to detect when the host is up this way
|
room.last_port = 0 # easier to detect when the host is up this way
|
||||||
if address:
|
if address:
|
||||||
room.timeout = original_timeout
|
|
||||||
room.last_activity = new_last_activity
|
room.last_activity = new_last_activity
|
||||||
print("timeout restored")
|
|
||||||
|
|
||||||
|
|
||||||
def set_room_timeout(room_id: str, timeout: float) -> None:
|
|
||||||
from pony.orm import db_session
|
|
||||||
|
|
||||||
from WebHostLib.models import Room
|
|
||||||
from WebHostLib import app
|
|
||||||
|
|
||||||
room_uuid = to_python(room_id)
|
|
||||||
with db_session:
|
|
||||||
room: Room = Room.get(id=room_uuid)
|
|
||||||
room.timeout = timeout
|
|
||||||
|
|
||||||
|
|
||||||
def get_multidata_for_room(webhost_client: "FlaskClient", room_id: str) -> bytes:
|
def get_multidata_for_room(webhost_client: "FlaskClient", room_id: str) -> bytes:
|
||||||
from pony.orm import db_session
|
from pony.orm import db_session
|
||||||
|
|
||||||
from WebHostLib.models import Room
|
from WebHostLib.models import Room
|
||||||
from WebHostLib import app
|
|
||||||
|
|
||||||
room_uuid = to_python(room_id)
|
room_uuid = to_python(room_id)
|
||||||
with db_session:
|
with db_session:
|
||||||
@@ -219,7 +200,6 @@ def set_multidata_for_room(webhost_client: "FlaskClient", room_id: str, data: by
|
|||||||
from pony.orm import db_session
|
from pony.orm import db_session
|
||||||
|
|
||||||
from WebHostLib.models import Room
|
from WebHostLib.models import Room
|
||||||
from WebHostLib import app
|
|
||||||
|
|
||||||
room_uuid = to_python(room_id)
|
room_uuid = to_python(room_id)
|
||||||
with db_session:
|
with db_session:
|
||||||
@@ -258,9 +238,11 @@ def _stop_webhost_mp(name_filter: str, graceful: bool = True) -> None:
|
|||||||
proc.kill()
|
proc.kill()
|
||||||
proc.join()
|
proc.join()
|
||||||
|
|
||||||
|
|
||||||
def stop_autogen(graceful: bool = True) -> None:
|
def stop_autogen(graceful: bool = True) -> None:
|
||||||
# FIXME: this name filter is jank, but there seems to be no way to add a custom prefix for a Pool
|
# FIXME: this name filter is jank, but there seems to be no way to add a custom prefix for a Pool
|
||||||
_stop_webhost_mp("SpawnPoolWorker-", graceful)
|
_stop_webhost_mp("SpawnPoolWorker-", graceful)
|
||||||
|
|
||||||
|
|
||||||
def stop_autohost(graceful: bool = True) -> None:
|
def stop_autohost(graceful: bool = True) -> None:
|
||||||
_stop_webhost_mp("MultiHoster", graceful)
|
_stop_webhost_mp("MultiHoster", graceful)
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ def copy(src: str, dst: str) -> None:
|
|||||||
src_cls = AutoWorldRegister.world_types[src]
|
src_cls = AutoWorldRegister.world_types[src]
|
||||||
src_folder = Path(src_cls.__file__).parent
|
src_folder = Path(src_cls.__file__).parent
|
||||||
worlds_folder = src_folder.parent
|
worlds_folder = src_folder.parent
|
||||||
if (not src_cls.__file__.endswith("__init__.py") or not src_folder.is_dir()
|
if (not src_cls.__file__.endswith(("__init__.py", "world.py")) or not src_folder.is_dir()
|
||||||
or not (worlds_folder / "generic").is_dir()):
|
or not (worlds_folder / "generic").is_dir()):
|
||||||
raise ValueError(f"Unsupported layout for copy_world from {src}")
|
raise ValueError(f"Unsupported layout for copy_world from {src}")
|
||||||
dst_folder = worlds_folder / dst_folder_name
|
dst_folder = worlds_folder / dst_folder_name
|
||||||
@@ -28,11 +28,14 @@ def copy(src: str, dst: str) -> None:
|
|||||||
raise ValueError(f"Destination {dst_folder} already exists")
|
raise ValueError(f"Destination {dst_folder} already exists")
|
||||||
shutil.copytree(src_folder, dst_folder)
|
shutil.copytree(src_folder, dst_folder)
|
||||||
_new_worlds[dst] = str(dst_folder)
|
_new_worlds[dst] = str(dst_folder)
|
||||||
with open(dst_folder / "__init__.py", "r", encoding="utf-8-sig") as f:
|
|
||||||
contents = f.read()
|
for potential_world_class_file in ("__init__.py", "world.py"):
|
||||||
contents = re.sub(r'game\s*(:\s*[a-zA-Z\[\]]+)?\s*=\s*[\'"]' + re.escape(src) + r'[\'"]', f'game = "{dst}"', contents)
|
with open(dst_folder / potential_world_class_file, "r", encoding="utf-8-sig") as f:
|
||||||
with open(dst_folder / "__init__.py", "w", encoding="utf-8") as f:
|
contents = f.read()
|
||||||
f.write(contents)
|
r_src = re.escape(src)
|
||||||
|
contents = re.sub(r'game\s*(:\s*[a-zA-Z\[\]]+)?\s*=\s*[\'"]' + r_src + r'[\'"]', f'game = "{dst}"', contents)
|
||||||
|
with open(dst_folder / "__init__.py", "w", encoding="utf-8") as f:
|
||||||
|
f.write(contents)
|
||||||
|
|
||||||
|
|
||||||
def delete(name: str) -> None:
|
def delete(name: str) -> None:
|
||||||
|
|||||||
@@ -1,14 +1,26 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
import abc
|
import abc
|
||||||
|
from bisect import bisect_right
|
||||||
|
from dataclasses import dataclass
|
||||||
|
import enum
|
||||||
import logging
|
import logging
|
||||||
from typing import TYPE_CHECKING, ClassVar, Dict, Iterable, Tuple, Any, Optional, Union, TypeGuard
|
from typing import (TYPE_CHECKING, Any, ClassVar, Dict, Generic, Iterable,
|
||||||
|
Optional, Sequence, Tuple, TypeGuard, TypeVar, Union)
|
||||||
|
|
||||||
|
|
||||||
from worlds.LauncherComponents import Component, SuffixIdentifier, Type, components
|
from worlds.LauncherComponents import Component, SuffixIdentifier, Type, components
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from SNIClient import SNIContext
|
from SNIClient import SNIContext
|
||||||
|
|
||||||
|
SNES_READ_CHUNK_SIZE = 2048
|
||||||
|
"""
|
||||||
|
note: SNI v0.0.101 currently has a bug where reads from
|
||||||
|
RetroArch >2048 bytes will only return the last ~2048 bytes read.
|
||||||
|
https://github.com/alttpo/sni/issues/51
|
||||||
|
"""
|
||||||
|
|
||||||
component = Component('SNI Client', 'SNIClient', component_type=Type.CLIENT, file_identifier=SuffixIdentifier(".apsoe"),
|
component = Component('SNI Client', 'SNIClient', component_type=Type.CLIENT, file_identifier=SuffixIdentifier(".apsoe"),
|
||||||
description="A client for connecting to SNES consoles via Super Nintendo Interface.")
|
description="A client for connecting to SNES consoles via Super Nintendo Interface.")
|
||||||
components.append(component)
|
components.append(component)
|
||||||
@@ -91,3 +103,119 @@ class SNIClient(abc.ABC, metaclass=AutoSNIClientRegister):
|
|||||||
def on_package(self, ctx: SNIContext, cmd: str, args: Dict[str, Any]) -> None:
|
def on_package(self, ctx: SNIContext, cmd: str, args: Dict[str, Any]) -> None:
|
||||||
""" override this with code to handle packages from the server """
|
""" override this with code to handle packages from the server """
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True, order=True)
|
||||||
|
class Read:
|
||||||
|
""" snes memory read - address and size in bytes """
|
||||||
|
address: int
|
||||||
|
size: int
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class _MemRead:
|
||||||
|
location: Read
|
||||||
|
data: bytes
|
||||||
|
|
||||||
|
|
||||||
|
_T_Enum = TypeVar("_T_Enum", bound=enum.Enum)
|
||||||
|
|
||||||
|
|
||||||
|
class SnesData(Generic[_T_Enum]):
|
||||||
|
_ranges: Sequence[_MemRead]
|
||||||
|
""" sorted by address """
|
||||||
|
|
||||||
|
def __init__(self, ranges: Sequence[tuple[Read, bytes]]) -> None:
|
||||||
|
self._ranges = [_MemRead(r, d) for r, d in ranges]
|
||||||
|
|
||||||
|
def get(self, read: _T_Enum) -> bytes:
|
||||||
|
assert isinstance(read.value, Read), read.value
|
||||||
|
address = read.value.address
|
||||||
|
index = bisect_right(self._ranges, address, key=lambda r: r.location.address) - 1
|
||||||
|
assert index >= 0, (self._ranges, read.value)
|
||||||
|
mem_read = self._ranges[index]
|
||||||
|
sub_index = address - mem_read.location.address
|
||||||
|
return mem_read.data[sub_index:sub_index + read.value.size]
|
||||||
|
|
||||||
|
|
||||||
|
class SnesReader(Generic[_T_Enum]):
|
||||||
|
"""
|
||||||
|
how to use:
|
||||||
|
```
|
||||||
|
from enum import Enum
|
||||||
|
from worlds.AutoSNIClient import Read, SNIClient, SnesReader
|
||||||
|
|
||||||
|
class MyGameMemory(Enum):
|
||||||
|
game_mode = Read(WRAM_START + 0x0998, 1)
|
||||||
|
send_queue = Read(SEND_QUEUE_START, 8 * 127)
|
||||||
|
...
|
||||||
|
|
||||||
|
snes_reader = SnesReader(MyGameMemory)
|
||||||
|
|
||||||
|
snes_data = await snes_reader.read(ctx)
|
||||||
|
if snes_data is None:
|
||||||
|
snes_logger.info("error reading from snes")
|
||||||
|
return
|
||||||
|
|
||||||
|
game_mode = snes_data.get(MyGameMemory.game_mode)
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
_ranges: Sequence[Read]
|
||||||
|
""" sorted by address """
|
||||||
|
|
||||||
|
def __init__(self, reads: type[_T_Enum]) -> None:
|
||||||
|
self._ranges = self._make_ranges(reads)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _make_ranges(reads: type[enum.Enum]) -> Sequence[Read]:
|
||||||
|
|
||||||
|
unprocessed_reads: list[Read] = []
|
||||||
|
for e in reads:
|
||||||
|
assert isinstance(e.value, Read), (reads.__name__, e, e.value)
|
||||||
|
unprocessed_reads.append(e.value)
|
||||||
|
unprocessed_reads.sort()
|
||||||
|
|
||||||
|
ranges: list[Read] = []
|
||||||
|
for read in unprocessed_reads:
|
||||||
|
# v end of the previous range
|
||||||
|
if len(ranges) == 0 or read.address - (ranges[-1].address + ranges[-1].size) > 255:
|
||||||
|
ranges.append(read)
|
||||||
|
else: # combine with previous range
|
||||||
|
chunk_address = ranges[-1].address
|
||||||
|
assert read.address >= chunk_address, "sort() didn't work? or something"
|
||||||
|
original_chunk_size = ranges[-1].size
|
||||||
|
new_size = max((read.address + read.size) - chunk_address,
|
||||||
|
original_chunk_size)
|
||||||
|
ranges[-1] = Read(chunk_address, new_size)
|
||||||
|
logging.debug(f"{len(ranges)=} {max(r.size for r in ranges)=}")
|
||||||
|
return ranges
|
||||||
|
|
||||||
|
async def read(self, ctx: "SNIContext") -> SnesData[_T_Enum] | None:
|
||||||
|
"""
|
||||||
|
returns `None` if reading fails,
|
||||||
|
otherwise returns the data for the registered `Enum`
|
||||||
|
"""
|
||||||
|
from SNIClient import snes_read
|
||||||
|
|
||||||
|
reads: list[tuple[Read, bytes]] = []
|
||||||
|
for r in self._ranges:
|
||||||
|
if r.size < SNES_READ_CHUNK_SIZE: # most common
|
||||||
|
response = await snes_read(ctx, r.address, r.size)
|
||||||
|
if response is None:
|
||||||
|
return None
|
||||||
|
reads.append((r, response))
|
||||||
|
else: # big read
|
||||||
|
# Problems were reported with big reads,
|
||||||
|
# so we chunk it into smaller pieces.
|
||||||
|
read_so_far = 0
|
||||||
|
collection: list[bytes] = []
|
||||||
|
while read_so_far < r.size:
|
||||||
|
remaining_size = r.size - read_so_far
|
||||||
|
chunk_size = min(SNES_READ_CHUNK_SIZE, remaining_size)
|
||||||
|
response = await snes_read(ctx, r.address + read_so_far, chunk_size)
|
||||||
|
if response is None:
|
||||||
|
return None
|
||||||
|
collection.append(response)
|
||||||
|
read_so_far += chunk_size
|
||||||
|
reads.append((r, b"".join(collection)))
|
||||||
|
return SnesData(reads)
|
||||||
|
|||||||
@@ -291,11 +291,11 @@ if not is_frozen():
|
|||||||
manifest = json.load(manifest_file)
|
manifest = json.load(manifest_file)
|
||||||
|
|
||||||
assert "game" in manifest, (
|
assert "game" in manifest, (
|
||||||
f"World directory {world_directory} has an archipelago.json manifest file, but it"
|
f"World directory {world_directory} has an archipelago.json manifest file, but it "
|
||||||
"does not define a \"game\"."
|
"does not define a \"game\"."
|
||||||
)
|
)
|
||||||
assert manifest["game"] == worldtype.game, (
|
assert manifest["game"] == worldtype.game, (
|
||||||
f"World directory {world_directory} has an archipelago.json manifest file, but value of the"
|
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})."
|
f"\"game\" field ({manifest['game']} does not equal the World class's game ({worldtype.game})."
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
@@ -318,5 +318,5 @@ if not is_frozen():
|
|||||||
open_folder(apworlds_folder)
|
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."))
|
description="Build APWorlds from loose-file world folders."))
|
||||||
|
|||||||
@@ -263,7 +263,6 @@ def generate_itempool(world):
|
|||||||
('Frog', 'Get Frog'),
|
('Frog', 'Get Frog'),
|
||||||
('Missing Smith', 'Return Smith'),
|
('Missing Smith', 'Return Smith'),
|
||||||
('Floodgate', 'Open Floodgate'),
|
('Floodgate', 'Open Floodgate'),
|
||||||
('Agahnim 1', 'Beat Agahnim 1'),
|
|
||||||
('Flute Activation Spot', 'Activated Flute'),
|
('Flute Activation Spot', 'Activated Flute'),
|
||||||
('Capacity Upgrade Shop', 'Capacity Upgrade Shop')
|
('Capacity Upgrade Shop', 'Capacity Upgrade Shop')
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -183,7 +183,7 @@ def check_enemizer(enemizercli):
|
|||||||
if getattr(check_enemizer, "done", None):
|
if getattr(check_enemizer, "done", None):
|
||||||
return
|
return
|
||||||
if not os.path.exists(enemizercli) and not os.path.exists(enemizercli + ".exe"):
|
if not os.path.exists(enemizercli) and not os.path.exists(enemizercli + ".exe"):
|
||||||
raise Exception(f"Enemizer not found at {enemizercli}, please install it."
|
raise Exception(f"Enemizer not found at {enemizercli}, please install it. "
|
||||||
f"Such as https://github.com/Ijwu/Enemizer/releases")
|
f"Such as https://github.com/Ijwu/Enemizer/releases")
|
||||||
|
|
||||||
with check_lock:
|
with check_lock:
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"game": "APQuest",
|
"game": "APQuest",
|
||||||
"minimum_ap_version": "0.6.4",
|
"minimum_ap_version": "0.6.4",
|
||||||
"world_version": "1.0.0",
|
"world_version": "1.0.1",
|
||||||
"authors": ["NewSoupVi"]
|
"authors": ["NewSoupVi"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ from collections.abc import Callable
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from math import sqrt
|
from math import sqrt
|
||||||
from random import choice, random
|
from random import choice, random
|
||||||
from typing import TYPE_CHECKING, Any
|
from typing import Any
|
||||||
|
|
||||||
from kivy.core.window import Keyboard, Window
|
from kivy.core.window import Keyboard, Window
|
||||||
from kivy.graphics import Color, Triangle
|
from kivy.graphics import Color, Triangle
|
||||||
|
|||||||
@@ -2,9 +2,8 @@ import pkgutil
|
|||||||
from collections.abc import Buffer
|
from collections.abc import Buffer
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from typing import Literal, NamedTuple, cast
|
from typing import Literal, NamedTuple, Protocol, cast
|
||||||
|
|
||||||
from bokeh.protocol import Protocol
|
|
||||||
from kivy.uix.image import CoreImage
|
from kivy.uix.image import CoreImage
|
||||||
|
|
||||||
from CommonClient import logger
|
from CommonClient import logger
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
from collections import Counter
|
from collections import Counter
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from typing import TYPE_CHECKING
|
|
||||||
|
|
||||||
from .events import Event, LocationClearedEvent, VictoryEvent
|
from .events import Event, LocationClearedEvent, VictoryEvent
|
||||||
from .gameboard import Gameboard
|
from .gameboard import Gameboard
|
||||||
|
|||||||
@@ -44,9 +44,10 @@ class CivVIContainer(APPlayerContainer):
|
|||||||
opened_zipfile.writestr(filename, yml)
|
opened_zipfile.writestr(filename, yml)
|
||||||
super().write_contents(opened_zipfile)
|
super().write_contents(opened_zipfile)
|
||||||
|
|
||||||
|
|
||||||
def sanitize_value(value: str) -> str:
|
def sanitize_value(value: str) -> str:
|
||||||
"""Removes values that can cause issues in XML"""
|
"""Removes values that can cause issues in XML"""
|
||||||
return value.replace('"', "'").replace('&', 'and')
|
return value.replace('"', "'").replace('&', 'and').replace('{', '').replace('}', '')
|
||||||
|
|
||||||
|
|
||||||
def get_cost(world: 'CivVIWorld', location: CivVILocationData) -> int:
|
def get_cost(world: 'CivVIWorld', location: CivVILocationData) -> int:
|
||||||
@@ -87,8 +88,10 @@ def generate_new_items(world: 'CivVIWorld') -> str:
|
|||||||
boost_civics = []
|
boost_civics = []
|
||||||
|
|
||||||
if world.options.boostsanity:
|
if world.options.boostsanity:
|
||||||
boost_techs = [location for location in locations if location.location_type == CivVICheckType.BOOST and location.name.split("_")[1] == "TECH"]
|
boost_techs = [location for location in locations if location.location_type ==
|
||||||
boost_civics = [location for location in locations if location.location_type == CivVICheckType.BOOST and location.name.split("_")[1] == "CIVIC"]
|
CivVICheckType.BOOST and location.name.split("_")[1] == "TECH"]
|
||||||
|
boost_civics = [location for location in locations if location.location_type ==
|
||||||
|
CivVICheckType.BOOST and location.name.split("_")[1] == "CIVIC"]
|
||||||
techs += boost_techs
|
techs += boost_techs
|
||||||
civics += boost_civics
|
civics += boost_civics
|
||||||
|
|
||||||
|
|||||||
@@ -214,7 +214,7 @@ renon_item_dialogue = {
|
|||||||
"\"Banshee Boomerang.\"",
|
"\"Banshee Boomerang.\"",
|
||||||
0x10: "No weapon triangle\n"
|
0x10: "No weapon triangle\n"
|
||||||
"advantages with this.",
|
"advantages with this.",
|
||||||
0x12: "It looks sus? Trust me,"
|
0x12: "It looks sus? Trust me,\n"
|
||||||
"my wares are genuine.",
|
"my wares are genuine.",
|
||||||
0x15: "This non-volatile kind\n"
|
0x15: "This non-volatile kind\n"
|
||||||
"is safe to handle.",
|
"is safe to handle.",
|
||||||
|
|||||||
@@ -1030,7 +1030,7 @@ def get_base_rom_bytes(file_name: str = "") -> bytes:
|
|||||||
basemd5 = hashlib.md5()
|
basemd5 = hashlib.md5()
|
||||||
basemd5.update(base_rom_bytes)
|
basemd5.update(base_rom_bytes)
|
||||||
if CV64_US_10_HASH != basemd5.hexdigest():
|
if CV64_US_10_HASH != basemd5.hexdigest():
|
||||||
raise Exception("Supplied Base Rom does not match known MD5 for Castlevania 64 US 1.0."
|
raise Exception("Supplied Base Rom does not match known MD5 for Castlevania 64 US 1.0. "
|
||||||
"Get the correct game and version, then dump it.")
|
"Get the correct game and version, then dump it.")
|
||||||
setattr(get_base_rom_bytes, "base_rom_bytes", base_rom_bytes)
|
setattr(get_base_rom_bytes, "base_rom_bytes", base_rom_bytes)
|
||||||
return base_rom_bytes
|
return base_rom_bytes
|
||||||
|
|||||||
@@ -247,6 +247,10 @@ class CastlevaniaCotMClient(BizHawkClient):
|
|||||||
await bizhawk.write(ctx.bizhawk_ctx, [(QUEUED_TEXTBOX_1_ADDRESS, [0 for _ in range(12)], "EWRAM")])
|
await bizhawk.write(ctx.bizhawk_ctx, [(QUEUED_TEXTBOX_1_ADDRESS, [0 for _ in range(12)], "EWRAM")])
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# If the player doesn't have Dash Boots for whatever reason, put them in their inventory now.
|
||||||
|
if not magic_items_array[0]:
|
||||||
|
await bizhawk.write(ctx.bizhawk_ctx, [(MAGIC_ITEMS_ARRAY_START, [1], "EWRAM")])
|
||||||
|
|
||||||
# Enable DeathLink if it's in our slot_data.
|
# Enable DeathLink if it's in our slot_data.
|
||||||
if "DeathLink" not in ctx.tags and ctx.slot_data["death_link"]:
|
if "DeathLink" not in ctx.tags and ctx.slot_data["death_link"]:
|
||||||
await ctx.update_death_link(True)
|
await ctx.update_death_link(True)
|
||||||
|
|||||||
@@ -586,7 +586,7 @@ def get_base_rom_bytes(file_name: str = "") -> bytes:
|
|||||||
basemd5.update(base_rom_bytes)
|
basemd5.update(base_rom_bytes)
|
||||||
# if basemd5.hexdigest() not in [CVCOTM_CT_US_HASH, CVCOTM_AC_US_HASH, CVCOTM_VC_US_HASH]:
|
# if basemd5.hexdigest() not in [CVCOTM_CT_US_HASH, CVCOTM_AC_US_HASH, CVCOTM_VC_US_HASH]:
|
||||||
if basemd5.hexdigest() not in [CVCOTM_CT_US_HASH, CVCOTM_AC_US_HASH]:
|
if basemd5.hexdigest() not in [CVCOTM_CT_US_HASH, CVCOTM_AC_US_HASH]:
|
||||||
raise Exception("Supplied Base ROM does not match known MD5s for Castlevania: Circle of the Moon USA."
|
raise Exception("Supplied Base ROM does not match known MD5s for Castlevania: Circle of the Moon USA. "
|
||||||
"Get the correct game and version, then dump it.")
|
"Get the correct game and version, then dump it.")
|
||||||
setattr(get_base_rom_bytes, "base_rom_bytes", base_rom_bytes)
|
setattr(get_base_rom_bytes, "base_rom_bytes", base_rom_bytes)
|
||||||
return base_rom_bytes
|
return base_rom_bytes
|
||||||
|
|||||||
@@ -90,6 +90,22 @@ class DarkSouls3World(World):
|
|||||||
self.created_regions = set()
|
self.created_regions = set()
|
||||||
self.all_excluded_locations.update(self.options.exclude_locations.value)
|
self.all_excluded_locations.update(self.options.exclude_locations.value)
|
||||||
|
|
||||||
|
# This code doesn't work because tests don't verify options
|
||||||
|
# Don't consider disabled locations to be AP-excluded
|
||||||
|
# if not self.options.enable_dlc:
|
||||||
|
# self.options.exclude_locations.value = {
|
||||||
|
# location
|
||||||
|
# for location in self.options.exclude_locations
|
||||||
|
# if not location_dictionary[location].dlc
|
||||||
|
# }
|
||||||
|
|
||||||
|
# if not self.options.enable_ngp:
|
||||||
|
# self.options.exclude_locations.value = {
|
||||||
|
# location for
|
||||||
|
# location in self.options.exclude_locations
|
||||||
|
# if not location_dictionary[location].ngp
|
||||||
|
# }
|
||||||
|
|
||||||
# Inform Universal Tracker where Yhorm is being randomized to.
|
# Inform Universal Tracker where Yhorm is being randomized to.
|
||||||
if hasattr(self.multiworld, "re_gen_passthrough"):
|
if hasattr(self.multiworld, "re_gen_passthrough"):
|
||||||
if "Dark Souls III" in self.multiworld.re_gen_passthrough:
|
if "Dark Souls III" in self.multiworld.re_gen_passthrough:
|
||||||
@@ -264,6 +280,13 @@ class DarkSouls3World(World):
|
|||||||
):
|
):
|
||||||
new_location.progress_type = LocationProgressType.EXCLUDED
|
new_location.progress_type = LocationProgressType.EXCLUDED
|
||||||
else:
|
else:
|
||||||
|
# Don't consider non-randomized locations to be AP-excluded
|
||||||
|
if location.name in excluded:
|
||||||
|
excluded.remove(location.name)
|
||||||
|
# Only remove from all_excluded if excluded does not have priority over missable
|
||||||
|
if not (self.options.missable_location_behavior < self.options.excluded_location_behavior):
|
||||||
|
self.all_excluded_locations.remove(location.name)
|
||||||
|
|
||||||
# Don't allow missable duplicates of progression items to be expected progression.
|
# Don't allow missable duplicates of progression items to be expected progression.
|
||||||
if location.name in self.missable_dupe_prog_locs: continue
|
if location.name in self.missable_dupe_prog_locs: continue
|
||||||
|
|
||||||
@@ -283,11 +306,6 @@ class DarkSouls3World(World):
|
|||||||
parent = new_region,
|
parent = new_region,
|
||||||
)
|
)
|
||||||
new_location.place_locked_item(event_item)
|
new_location.place_locked_item(event_item)
|
||||||
if location.name in excluded:
|
|
||||||
excluded.remove(location.name)
|
|
||||||
# Only remove from all_excluded if excluded does not have priority over missable
|
|
||||||
if not (self.options.missable_location_behavior < self.options.excluded_location_behavior):
|
|
||||||
self.all_excluded_locations.remove(location.name)
|
|
||||||
|
|
||||||
new_region.locations.append(new_location)
|
new_region.locations.append(new_location)
|
||||||
|
|
||||||
@@ -1357,7 +1375,7 @@ class DarkSouls3World(World):
|
|||||||
if self.yhorm_location != default_yhorm_location:
|
if self.yhorm_location != default_yhorm_location:
|
||||||
text += f"\nYhorm takes the place of {self.yhorm_location.name} in {self.player_name}'s world\n"
|
text += f"\nYhorm takes the place of {self.yhorm_location.name} in {self.player_name}'s world\n"
|
||||||
|
|
||||||
if self.options.excluded_location_behavior == "allow_useful":
|
if self.options.excluded_location_behavior != "forbid_useful":
|
||||||
text += f"\n{self.player_name}'s world excluded: {sorted(self.all_excluded_locations)}\n"
|
text += f"\n{self.player_name}'s world excluded: {sorted(self.all_excluded_locations)}\n"
|
||||||
|
|
||||||
if text:
|
if text:
|
||||||
|
|||||||
@@ -584,7 +584,7 @@ def launch(*new_args: str):
|
|||||||
|
|
||||||
# args handling
|
# args handling
|
||||||
parser = get_base_parser(description="Optional arguments to Factorio Client follow. "
|
parser = get_base_parser(description="Optional arguments to Factorio Client follow. "
|
||||||
"Remaining arguments get passed into bound Factorio instance."
|
"Remaining arguments get passed into bound Factorio instance. "
|
||||||
"Refer to Factorio --help for those.")
|
"Refer to Factorio --help for those.")
|
||||||
parser.add_argument('--rcon-port', default='24242', type=int, help='Port to use to communicate with Factorio')
|
parser.add_argument('--rcon-port', default='24242', type=int, help='Port to use to communicate with Factorio')
|
||||||
parser.add_argument('--rcon-password', help='Password to authenticate with RCON.')
|
parser.add_argument('--rcon-password', help='Password to authenticate with RCON.')
|
||||||
|
|||||||
@@ -155,6 +155,10 @@ def set_rules(self) -> None:
|
|||||||
return True
|
return True
|
||||||
check_foresta(loc.parent_region)
|
check_foresta(loc.parent_region)
|
||||||
|
|
||||||
|
if self.options.map_shuffle or self.options.crest_shuffle:
|
||||||
|
process_rules(self.multiworld.get_entrance("Subregion Frozen Fields to Subregion Aquaria", self.player),
|
||||||
|
["SummerAquaria"])
|
||||||
|
|
||||||
if self.options.logic == "friendly":
|
if self.options.logic == "friendly":
|
||||||
process_rules(self.multiworld.get_entrance("Overworld - Ice Pyramid", self.player),
|
process_rules(self.multiworld.get_entrance("Overworld - Ice Pyramid", self.player),
|
||||||
["MagicMirror"])
|
["MagicMirror"])
|
||||||
|
|||||||
@@ -601,7 +601,7 @@ async def run_game(ctx: JakAndDaxterContext):
|
|||||||
f"Please check your host.yaml file.\n"
|
f"Please check your host.yaml file.\n"
|
||||||
f"If the value of 'jakanddaxter_options > auto_detect_root_directory' is true, verify that OpenGOAL "
|
f"If the value of 'jakanddaxter_options > auto_detect_root_directory' is true, verify that OpenGOAL "
|
||||||
f"is installed properly.\n"
|
f"is installed properly.\n"
|
||||||
f"If it is false, check the value of 'jakanddaxter_options > root_directory'."
|
f"If it is false, check the value of 'jakanddaxter_options > root_directory'. "
|
||||||
f"Verify it is a valid existing path, and all backslashes have been replaced with forward slashes.")
|
f"Verify it is a valid existing path, and all backslashes have been replaced with forward slashes.")
|
||||||
ctx.on_log_error(logger, msg)
|
ctx.on_log_error(logger, msg)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ class TradesCostNothingTest(JakAndDaxterTestBase):
|
|||||||
"global_orbsanity_bundle_size": 10,
|
"global_orbsanity_bundle_size": 10,
|
||||||
"citizen_orb_trade_amount": 0,
|
"citizen_orb_trade_amount": 0,
|
||||||
"oracle_orb_trade_amount": 0,
|
"oracle_orb_trade_amount": 0,
|
||||||
"start_inventory": {"Power Cell": 100},
|
"fire_canyon_cell_count": 0,
|
||||||
|
"mountain_pass_cell_count": 0,
|
||||||
|
"lava_tube_cell_count": 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
def test_orb_items_are_filler(self):
|
def test_orb_items_are_filler(self):
|
||||||
@@ -26,7 +28,9 @@ class TradesCostEverythingTest(JakAndDaxterTestBase):
|
|||||||
"global_orbsanity_bundle_size": 10,
|
"global_orbsanity_bundle_size": 10,
|
||||||
"citizen_orb_trade_amount": 120,
|
"citizen_orb_trade_amount": 120,
|
||||||
"oracle_orb_trade_amount": 150,
|
"oracle_orb_trade_amount": 150,
|
||||||
"start_inventory": {"Power Cell": 100},
|
"fire_canyon_cell_count": 0,
|
||||||
|
"mountain_pass_cell_count": 0,
|
||||||
|
"lava_tube_cell_count": 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
def test_orb_items_are_progression(self):
|
def test_orb_items_are_progression(self):
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ class KH1Web(WebWorld):
|
|||||||
theme = "ocean"
|
theme = "ocean"
|
||||||
tutorials = [Tutorial(
|
tutorials = [Tutorial(
|
||||||
"Multiworld Setup Guide",
|
"Multiworld Setup Guide",
|
||||||
"A guide to setting up the Kingdom Hearts Randomizer software on your computer."
|
"A guide to setting up the Kingdom Hearts Randomizer software on your computer. "
|
||||||
"This guide covers single-player, multiworld, and related software.",
|
"This guide covers single-player, multiworld, and related software.",
|
||||||
"English",
|
"English",
|
||||||
"kh1_en.md",
|
"kh1_en.md",
|
||||||
|
|||||||
@@ -195,21 +195,24 @@ class MagpieBridge:
|
|||||||
async def handler(self, websocket):
|
async def handler(self, websocket):
|
||||||
self.ws = websocket
|
self.ws = websocket
|
||||||
while True:
|
while True:
|
||||||
message = json.loads(await websocket.recv())
|
try:
|
||||||
if message["type"] == "handshake":
|
message = json.loads(await websocket.recv())
|
||||||
logger.info(
|
if message["type"] == "handshake":
|
||||||
f"Connected, supported features: {message['features']}")
|
logger.info(
|
||||||
self.features = message["features"]
|
f"Connected, supported features: {message['features']}")
|
||||||
|
self.features = message["features"]
|
||||||
|
|
||||||
await self.send_handshAck()
|
await self.send_handshAck()
|
||||||
|
|
||||||
if message["type"] == "sendFull":
|
if message["type"] == "sendFull":
|
||||||
if "items" in self.features:
|
if "items" in self.features:
|
||||||
await self.send_all_inventory()
|
await self.send_all_inventory()
|
||||||
if "checks" in self.features:
|
if "checks" in self.features:
|
||||||
await self.send_all_checks()
|
await self.send_all_checks()
|
||||||
if self.use_entrance_tracker():
|
if self.use_entrance_tracker():
|
||||||
await self.send_gps(diff=False)
|
await self.send_gps(diff=False)
|
||||||
|
except websockets.exceptions.ConnectionClosedOK:
|
||||||
|
pass
|
||||||
|
|
||||||
# Translate renamed IDs back to LADXR IDs
|
# Translate renamed IDs back to LADXR IDs
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ class MLSSClient(BizHawkClient):
|
|||||||
if seed not in ctx.seed_name:
|
if seed not in ctx.seed_name:
|
||||||
logger.info(
|
logger.info(
|
||||||
"ERROR: The ROM you loaded is for a different game of AP. "
|
"ERROR: The ROM you loaded is for a different game of AP. "
|
||||||
"Please make sure the host has sent you the correct patch file,"
|
"Please make sure the host has sent you the correct patch file, "
|
||||||
"and that you have opened the correct ROM."
|
"and that you have opened the correct ROM."
|
||||||
)
|
)
|
||||||
raise bizhawk.ConnectorError("Loaded ROM is for Incorrect lobby.")
|
raise bizhawk.ConnectorError("Loaded ROM is for Incorrect lobby.")
|
||||||
|
|||||||
@@ -775,7 +775,7 @@ item_descriptions = {
|
|||||||
item_names.BULLFROG_BROODLINGS: "Bullfrogs spawn two broodlings on impact, in addition to unloading their cargo.",
|
item_names.BULLFROG_BROODLINGS: "Bullfrogs spawn two broodlings on impact, in addition to unloading their cargo.",
|
||||||
item_names.BULLFROG_HARD_IMPACT: "Bullfrogs deal more damage and stun longer on impact.",
|
item_names.BULLFROG_HARD_IMPACT: "Bullfrogs deal more damage and stun longer on impact.",
|
||||||
item_names.INFESTED_BANSHEE_BRACED_EXOSKELETON: "Infested Banshees gain +100 life.",
|
item_names.INFESTED_BANSHEE_BRACED_EXOSKELETON: "Infested Banshees gain +100 life.",
|
||||||
item_names.INFESTED_BANSHEE_RAPID_HIBERNATION: "Infested Banshees regenerate 20 life and energy per second while burrowed.",
|
item_names.INFESTED_BANSHEE_RAPID_HIBERNATION: "Allows Infested Banshees to Burrow. Infested Banshees regenerate 20 life and energy per second while burrowed.",
|
||||||
item_names.INFESTED_BANSHEE_FLESHFUSED_TARGETING_OPTICS: "Infested Banshees gain +2 range while cloaked.",
|
item_names.INFESTED_BANSHEE_FLESHFUSED_TARGETING_OPTICS: "Infested Banshees gain +2 range while cloaked.",
|
||||||
item_names.INFESTED_LIBERATOR_CLOUD_DISPERSAL: "Infested Liberators instantly transform into a cloud of microscopic organisms while attacking, reducing the damage they take by 85%.",
|
item_names.INFESTED_LIBERATOR_CLOUD_DISPERSAL: "Infested Liberators instantly transform into a cloud of microscopic organisms while attacking, reducing the damage they take by 85%.",
|
||||||
item_names.INFESTED_LIBERATOR_VIRAL_CONTAMINATION: "Increases the damage Infested Liberators deal to their primary target by 100%.",
|
item_names.INFESTED_LIBERATOR_VIRAL_CONTAMINATION: "Increases the damage Infested Liberators deal to their primary target by 100%.",
|
||||||
|
|||||||
@@ -111,6 +111,9 @@ class ItemGroupNames:
|
|||||||
TERRAN_ORIGINAL_PROGRESSIVE_UPGRADES = "Terran Original Progressive Upgrades"
|
TERRAN_ORIGINAL_PROGRESSIVE_UPGRADES = "Terran Original Progressive Upgrades"
|
||||||
"""Progressive items where level 1 appeared in WoL"""
|
"""Progressive items where level 1 appeared in WoL"""
|
||||||
MENGSK_UNITS = "Mengsk Units"
|
MENGSK_UNITS = "Mengsk Units"
|
||||||
|
TERRAN_SC1_UNITS = "Terran SC1 Units"
|
||||||
|
TERRAN_SC1_BUILDINGS = "Terran SC1 Buildings"
|
||||||
|
TERRAN_LADDER_UNITS = "Terran Ladder Units"
|
||||||
TERRAN_VETERANCY_UNITS = "Terran Veterancy Units"
|
TERRAN_VETERANCY_UNITS = "Terran Veterancy Units"
|
||||||
ORBITAL_COMMAND_ABILITIES = "Orbital Command Abilities"
|
ORBITAL_COMMAND_ABILITIES = "Orbital Command Abilities"
|
||||||
WOL_ORBITAL_COMMAND_ABILITIES = "WoL Command Center Abilities"
|
WOL_ORBITAL_COMMAND_ABILITIES = "WoL Command Center Abilities"
|
||||||
@@ -154,6 +157,8 @@ class ItemGroupNames:
|
|||||||
"""All items from Stukov co-op subfaction"""
|
"""All items from Stukov co-op subfaction"""
|
||||||
INF_TERRAN_UNITS = "Infested Terran Units"
|
INF_TERRAN_UNITS = "Infested Terran Units"
|
||||||
INF_TERRAN_UPGRADES = "Infested Terran Upgrades"
|
INF_TERRAN_UPGRADES = "Infested Terran Upgrades"
|
||||||
|
ZERG_SC1_UNITS = "Zerg SC1 Units"
|
||||||
|
ZERG_LADDER_UNITS = "Zerg Ladder Units"
|
||||||
|
|
||||||
PROTOSS_ITEMS = "Protoss Items"
|
PROTOSS_ITEMS = "Protoss Items"
|
||||||
PROTOSS_UNITS = "Protoss Units"
|
PROTOSS_UNITS = "Protoss Units"
|
||||||
@@ -176,6 +181,9 @@ class ItemGroupNames:
|
|||||||
NERAZIM_UNITS = "Nerazim"
|
NERAZIM_UNITS = "Nerazim"
|
||||||
TAL_DARIM_UNITS = "Tal'Darim"
|
TAL_DARIM_UNITS = "Tal'Darim"
|
||||||
PURIFIER_UNITS = "Purifier"
|
PURIFIER_UNITS = "Purifier"
|
||||||
|
PROTOSS_SC1_UNITS = "Protoss SC1 Units"
|
||||||
|
PROTOSS_SC1_BUILDINGS = "Protoss SC1 Buildings"
|
||||||
|
PROTOSS_LADDER_UNITS = "Protoss Ladder Units"
|
||||||
|
|
||||||
VANILLA_ITEMS = "Vanilla Items"
|
VANILLA_ITEMS = "Vanilla Items"
|
||||||
OVERPOWERED_ITEMS = "Overpowered Items"
|
OVERPOWERED_ITEMS = "Overpowered Items"
|
||||||
@@ -288,8 +296,14 @@ item_name_groups[ItemGroupNames.WOL_BUILDINGS] = wol_buildings = [
|
|||||||
item_names.HIVE_MIND_EMULATOR, item_names.PSI_DISRUPTER,
|
item_names.HIVE_MIND_EMULATOR, item_names.PSI_DISRUPTER,
|
||||||
]
|
]
|
||||||
item_name_groups[ItemGroupNames.TERRAN_BUILDINGS] = terran_buildings = [
|
item_name_groups[ItemGroupNames.TERRAN_BUILDINGS] = terran_buildings = [
|
||||||
item_name for item_name, item_data in item_tables.item_table.items()
|
*[
|
||||||
if item_data.type == item_tables.TerranItemType.Building or item_name in wol_buildings
|
item_name for item_name, item_data in item_tables.item_table.items()
|
||||||
|
if item_data.type == item_tables.TerranItemType.Building or item_name in wol_buildings
|
||||||
|
],
|
||||||
|
item_names.PSI_SCREEN,
|
||||||
|
item_names.SONIC_DISRUPTER,
|
||||||
|
item_names.PSI_INDOCTRINATOR,
|
||||||
|
item_names.ARGUS_AMPLIFIER,
|
||||||
]
|
]
|
||||||
item_name_groups[ItemGroupNames.MENGSK_UNITS] = [
|
item_name_groups[ItemGroupNames.MENGSK_UNITS] = [
|
||||||
item_names.AEGIS_GUARD, item_names.EMPERORS_SHADOW,
|
item_names.AEGIS_GUARD, item_names.EMPERORS_SHADOW,
|
||||||
@@ -317,6 +331,41 @@ spider_mine_sources = [
|
|||||||
item_names.SIEGE_TANK_SPIDER_MINES,
|
item_names.SIEGE_TANK_SPIDER_MINES,
|
||||||
item_names.RAVEN_SPIDER_MINES,
|
item_names.RAVEN_SPIDER_MINES,
|
||||||
]
|
]
|
||||||
|
item_name_groups[ItemGroupNames.TERRAN_SC1_UNITS] = [
|
||||||
|
item_names.MARINE,
|
||||||
|
item_names.FIREBAT,
|
||||||
|
item_names.GHOST,
|
||||||
|
item_names.MEDIC,
|
||||||
|
item_names.VULTURE,
|
||||||
|
item_names.SIEGE_TANK,
|
||||||
|
item_names.GOLIATH,
|
||||||
|
item_names.WRAITH,
|
||||||
|
# No dropship
|
||||||
|
item_names.SCIENCE_VESSEL,
|
||||||
|
item_names.BATTLECRUISER,
|
||||||
|
item_names.VALKYRIE,
|
||||||
|
]
|
||||||
|
item_name_groups[ItemGroupNames.TERRAN_SC1_BUILDINGS] = [
|
||||||
|
item_names.BUNKER,
|
||||||
|
item_names.MISSILE_TURRET,
|
||||||
|
]
|
||||||
|
item_name_groups[ItemGroupNames.TERRAN_LADDER_UNITS] = [
|
||||||
|
item_names.MARINE,
|
||||||
|
item_names.MARAUDER,
|
||||||
|
item_names.REAPER,
|
||||||
|
item_names.GHOST,
|
||||||
|
item_names.HELLION,
|
||||||
|
item_names.WIDOW_MINE,
|
||||||
|
item_names.SIEGE_TANK,
|
||||||
|
item_names.THOR,
|
||||||
|
item_names.CYCLONE,
|
||||||
|
item_names.VIKING,
|
||||||
|
item_names.MEDIVAC,
|
||||||
|
item_names.LIBERATOR,
|
||||||
|
item_names.RAVEN,
|
||||||
|
item_names.BANSHEE,
|
||||||
|
item_names.BATTLECRUISER,
|
||||||
|
]
|
||||||
|
|
||||||
# Terran Upgrades
|
# Terran Upgrades
|
||||||
item_name_groups[ItemGroupNames.WOL_UPGRADES] = wol_upgrades = [
|
item_name_groups[ItemGroupNames.WOL_UPGRADES] = wol_upgrades = [
|
||||||
@@ -597,6 +646,38 @@ item_name_groups[ItemGroupNames.OVERLORD_UPGRADES] = [
|
|||||||
item_names.OVERLORD_IMPROVED_OVERLORDS,
|
item_names.OVERLORD_IMPROVED_OVERLORDS,
|
||||||
item_names.OVERLORD_OVERSEER_ASPECT,
|
item_names.OVERLORD_OVERSEER_ASPECT,
|
||||||
]
|
]
|
||||||
|
item_name_groups[ItemGroupNames.ZERG_SC1_UNITS] = [
|
||||||
|
item_names.ZERGLING,
|
||||||
|
item_names.HYDRALISK,
|
||||||
|
item_names.MUTALISK,
|
||||||
|
item_names.SCOURGE,
|
||||||
|
item_names.BROOD_QUEEN,
|
||||||
|
item_names.DEFILER,
|
||||||
|
item_names.ULTRALISK,
|
||||||
|
item_names.HYDRALISK_LURKER_ASPECT,
|
||||||
|
item_names.MUTALISK_CORRUPTOR_DEVOURER_ASPECT,
|
||||||
|
item_names.MUTALISK_CORRUPTOR_GUARDIAN_ASPECT,
|
||||||
|
item_names.DEVOURING_ONES,
|
||||||
|
item_names.HUNTER_KILLERS,
|
||||||
|
item_names.TORRASQUE_MERC,
|
||||||
|
]
|
||||||
|
item_name_groups[ItemGroupNames.ZERG_LADDER_UNITS] = [
|
||||||
|
item_names.ZERGLING,
|
||||||
|
item_names.SWARM_QUEEN, # Replace: Hive Queen
|
||||||
|
item_names.ZERGLING_BANELING_ASPECT,
|
||||||
|
item_names.ROACH,
|
||||||
|
item_names.ROACH_RAVAGER_ASPECT,
|
||||||
|
item_names.OVERLORD_OVERSEER_ASPECT,
|
||||||
|
item_names.HYDRALISK,
|
||||||
|
item_names.HYDRALISK_LURKER_ASPECT,
|
||||||
|
item_names.MUTALISK,
|
||||||
|
item_names.CORRUPTOR,
|
||||||
|
item_names.MUTALISK_CORRUPTOR_VIPER_ASPECT,
|
||||||
|
item_names.MUTALISK_CORRUPTOR_BROOD_LORD_ASPECT,
|
||||||
|
item_names.INFESTOR,
|
||||||
|
item_names.SWARM_HOST,
|
||||||
|
item_names.ULTRALISK,
|
||||||
|
]
|
||||||
|
|
||||||
# Zerg Upgrades
|
# Zerg Upgrades
|
||||||
item_name_groups[ItemGroupNames.HOTS_STRAINS] = hots_strains = [
|
item_name_groups[ItemGroupNames.HOTS_STRAINS] = hots_strains = [
|
||||||
@@ -826,6 +907,45 @@ item_name_groups[ItemGroupNames.LOTV_ITEMS] = vanilla_lotv_items = (
|
|||||||
+ protoss_generic_upgrades
|
+ protoss_generic_upgrades
|
||||||
+ lotv_war_council_upgrades
|
+ lotv_war_council_upgrades
|
||||||
)
|
)
|
||||||
|
item_name_groups[ItemGroupNames.PROTOSS_SC1_UNITS] = [
|
||||||
|
item_names.ZEALOT,
|
||||||
|
item_names.DRAGOON,
|
||||||
|
item_names.HIGH_TEMPLAR,
|
||||||
|
item_names.DARK_TEMPLAR,
|
||||||
|
item_names.DARK_ARCHON,
|
||||||
|
item_names.DARK_TEMPLAR_DARK_ARCHON_MELD,
|
||||||
|
# No shuttle
|
||||||
|
item_names.REAVER,
|
||||||
|
item_names.OBSERVER,
|
||||||
|
item_names.SCOUT,
|
||||||
|
item_names.CARRIER,
|
||||||
|
item_names.ARBITER,
|
||||||
|
item_names.CORSAIR,
|
||||||
|
]
|
||||||
|
item_name_groups[ItemGroupNames.PROTOSS_SC1_BUILDINGS] = [
|
||||||
|
item_names.PHOTON_CANNON,
|
||||||
|
item_names.SHIELD_BATTERY,
|
||||||
|
]
|
||||||
|
item_name_groups[ItemGroupNames.PROTOSS_LADDER_UNITS] = [
|
||||||
|
item_names.ZEALOT,
|
||||||
|
item_names.STALKER,
|
||||||
|
item_names.SENTRY,
|
||||||
|
item_names.ADEPT,
|
||||||
|
item_names.HIGH_TEMPLAR,
|
||||||
|
item_names.DARK_TEMPLAR,
|
||||||
|
item_names.DARK_TEMPLAR_ARCHON_MERGE,
|
||||||
|
item_names.OBSERVER,
|
||||||
|
item_names.WARP_PRISM,
|
||||||
|
item_names.IMMORTAL,
|
||||||
|
item_names.COLOSSUS,
|
||||||
|
item_names.DISRUPTOR,
|
||||||
|
item_names.PHOENIX,
|
||||||
|
item_names.VOID_RAY,
|
||||||
|
item_names.ORACLE,
|
||||||
|
item_names.CARRIER,
|
||||||
|
item_names.TEMPEST,
|
||||||
|
item_names.MOTHERSHIP, # Replace: Aiur Mothership
|
||||||
|
]
|
||||||
|
|
||||||
item_name_groups[ItemGroupNames.VANILLA_ITEMS] = vanilla_items = (
|
item_name_groups[ItemGroupNames.VANILLA_ITEMS] = vanilla_items = (
|
||||||
vanilla_wol_items + vanilla_hots_items + vanilla_lotv_items
|
vanilla_wol_items + vanilla_hots_items + vanilla_lotv_items
|
||||||
|
|||||||
@@ -2151,127 +2151,6 @@ not_balanced_starting_units = {
|
|||||||
item_names.TEMPEST,
|
item_names.TEMPEST,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
# Defense rating table
|
|
||||||
# Commented defense ratings are handled in LogicMixin
|
|
||||||
tvx_defense_ratings = {
|
|
||||||
item_names.SIEGE_TANK: 5,
|
|
||||||
# "Graduating Range": 1,
|
|
||||||
item_names.PLANETARY_FORTRESS: 3,
|
|
||||||
# Bunker w/ Marine/Marauder: 3,
|
|
||||||
item_names.PERDITION_TURRET: 2,
|
|
||||||
item_names.DEVASTATOR_TURRET: 2,
|
|
||||||
item_names.VULTURE: 1,
|
|
||||||
item_names.BANSHEE: 1,
|
|
||||||
item_names.BATTLECRUISER: 1,
|
|
||||||
item_names.LIBERATOR: 4,
|
|
||||||
item_names.WIDOW_MINE: 1,
|
|
||||||
# "Concealment (Widow Mine)": 1
|
|
||||||
}
|
|
||||||
tvz_defense_ratings = {
|
|
||||||
item_names.PERDITION_TURRET: 2,
|
|
||||||
# Bunker w/ Firebat: 2,
|
|
||||||
item_names.LIBERATOR: -2,
|
|
||||||
item_names.HIVE_MIND_EMULATOR: 3,
|
|
||||||
item_names.PSI_DISRUPTER: 3,
|
|
||||||
}
|
|
||||||
tvx_air_defense_ratings = {
|
|
||||||
item_names.MISSILE_TURRET: 2,
|
|
||||||
}
|
|
||||||
zvx_defense_ratings = {
|
|
||||||
# Note that this doesn't include Kerrigan because this is just for race swaps, which doesn't involve her (for now)
|
|
||||||
item_names.SPINE_CRAWLER: 3,
|
|
||||||
# w/ Twin Drones: 1
|
|
||||||
item_names.SWARM_QUEEN: 1,
|
|
||||||
item_names.SWARM_HOST: 1,
|
|
||||||
# impaler: 3
|
|
||||||
# "Hardened Tentacle Spines (Impaler)": 2
|
|
||||||
# lurker: 1
|
|
||||||
# "Seismic Spines (Lurker)": 2
|
|
||||||
# "Adapted Spines (Lurker)": 1
|
|
||||||
# brood lord : 2
|
|
||||||
# corpser roach: 1
|
|
||||||
# creep tumors (swarm queen or overseer): 1
|
|
||||||
# w/ malignant creep: 1
|
|
||||||
# tanks with ammo: 5
|
|
||||||
item_names.INFESTED_BUNKER: 3,
|
|
||||||
item_names.BILE_LAUNCHER: 2,
|
|
||||||
}
|
|
||||||
# zvz_defense_ratings = {
|
|
||||||
# corpser roach: 1
|
|
||||||
# primal igniter: 2
|
|
||||||
# lurker: 1
|
|
||||||
# w/ adapted spines: -1
|
|
||||||
# impaler: -1
|
|
||||||
# }
|
|
||||||
zvx_air_defense_ratings = {
|
|
||||||
item_names.SPORE_CRAWLER: 2,
|
|
||||||
# w/ Twin Drones: 1
|
|
||||||
item_names.INFESTED_MISSILE_TURRET: 2,
|
|
||||||
}
|
|
||||||
pvx_defense_ratings = {
|
|
||||||
item_names.PHOTON_CANNON: 2,
|
|
||||||
item_names.KHAYDARIN_MONOLITH: 3,
|
|
||||||
item_names.SHIELD_BATTERY: 1,
|
|
||||||
item_names.NEXUS_OVERCHARGE: 2,
|
|
||||||
item_names.SKYLORD: 1,
|
|
||||||
item_names.MATRIX_OVERLOAD: 1,
|
|
||||||
item_names.COLOSSUS: 1,
|
|
||||||
item_names.VANGUARD: 1,
|
|
||||||
item_names.REAVER: 1,
|
|
||||||
}
|
|
||||||
pvz_defense_ratings = {
|
|
||||||
item_names.KHAYDARIN_MONOLITH: -2,
|
|
||||||
item_names.COLOSSUS: 1,
|
|
||||||
}
|
|
||||||
|
|
||||||
terran_passive_ratings = {
|
|
||||||
item_names.AUTOMATED_REFINERY: 4,
|
|
||||||
item_names.COMMAND_CENTER_MULE: 4,
|
|
||||||
item_names.ORBITAL_DEPOTS: 2,
|
|
||||||
item_names.COMMAND_CENTER_COMMAND_CENTER_REACTOR: 2,
|
|
||||||
item_names.COMMAND_CENTER_EXTRA_SUPPLIES: 2,
|
|
||||||
item_names.MICRO_FILTERING: 2,
|
|
||||||
item_names.TECH_REACTOR: 2
|
|
||||||
}
|
|
||||||
|
|
||||||
zerg_passive_ratings = {
|
|
||||||
item_names.TWIN_DRONES: 7,
|
|
||||||
item_names.AUTOMATED_EXTRACTORS: 4,
|
|
||||||
item_names.VESPENE_EFFICIENCY: 3,
|
|
||||||
item_names.OVERLORD_IMPROVED_OVERLORDS: 4,
|
|
||||||
item_names.MALIGNANT_CREEP: 2
|
|
||||||
}
|
|
||||||
|
|
||||||
protoss_passive_ratings = {
|
|
||||||
item_names.QUATRO: 4,
|
|
||||||
item_names.ORBITAL_ASSIMILATORS: 4,
|
|
||||||
item_names.AMPLIFIED_ASSIMILATORS: 3,
|
|
||||||
item_names.PROBE_WARPIN: 2,
|
|
||||||
item_names.ELDER_PROBES: 2,
|
|
||||||
item_names.MATRIX_OVERLOAD: 2
|
|
||||||
}
|
|
||||||
|
|
||||||
soa_energy_ratings = {
|
|
||||||
item_names.SOA_SOLAR_LANCE: 8,
|
|
||||||
item_names.SOA_DEPLOY_FENIX: 7,
|
|
||||||
item_names.SOA_TEMPORAL_FIELD: 6,
|
|
||||||
item_names.SOA_PROGRESSIVE_PROXY_PYLON: 5, # Requires Lvl 2 (Warp in Reinforcements)
|
|
||||||
item_names.SOA_SHIELD_OVERCHARGE: 5,
|
|
||||||
item_names.SOA_ORBITAL_STRIKE: 4
|
|
||||||
}
|
|
||||||
|
|
||||||
soa_passive_ratings = {
|
|
||||||
item_names.GUARDIAN_SHELL: 4,
|
|
||||||
item_names.OVERWATCH: 2
|
|
||||||
}
|
|
||||||
|
|
||||||
soa_ultimate_ratings = {
|
|
||||||
item_names.SOA_TIME_STOP: 4,
|
|
||||||
item_names.SOA_PURIFIER_BEAM: 3,
|
|
||||||
item_names.SOA_SOLAR_BOMBARDMENT: 3
|
|
||||||
}
|
|
||||||
|
|
||||||
kerrigan_levels = [
|
kerrigan_levels = [
|
||||||
item_name for item_name, item_data in item_table.items()
|
item_name for item_name, item_data in item_table.items()
|
||||||
if item_data.type == ZergItemType.Level and item_data.race == SC2Race.ZERG
|
if item_data.type == ZergItemType.Level and item_data.race == SC2Race.ZERG
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -306,8 +306,8 @@ class ShapezWorld(World):
|
|||||||
self.location_count = len(self.included_locations)
|
self.location_count = len(self.included_locations)
|
||||||
|
|
||||||
# Create regions and entrances based on included locations and player options
|
# Create regions and entrances based on included locations and player options
|
||||||
self.multiworld.regions.extend(create_shapez_regions(self.player, self.multiworld,
|
has_floating = self.options.allow_floating_layers.value or not (self.options.randomize_level_requirements and self.options.randomize_upgrade_requirements)
|
||||||
bool(self.options.allow_floating_layers.value),
|
self.multiworld.regions.extend(create_shapez_regions(self.player, self.multiworld, has_floating,
|
||||||
self.included_locations, self.location_name_to_id,
|
self.included_locations, self.location_name_to_id,
|
||||||
self.level_logic, self.upgrade_logic,
|
self.level_logic, self.upgrade_logic,
|
||||||
self.options.early_balancer_tunnel_and_trash.current_key,
|
self.options.early_balancer_tunnel_and_trash.current_key,
|
||||||
|
|||||||
@@ -1047,7 +1047,7 @@ def set_er_region_rules(world: "TunicWorld", regions: dict[str, Region], portal_
|
|||||||
connecting_region=regions["Rooted Ziggurat Portal Room"])
|
connecting_region=regions["Rooted Ziggurat Portal Room"])
|
||||||
regions["Rooted Ziggurat Portal Room"].connect(
|
regions["Rooted Ziggurat Portal Room"].connect(
|
||||||
connecting_region=regions["Rooted Ziggurat Portal"],
|
connecting_region=regions["Rooted Ziggurat Portal"],
|
||||||
rule=lambda state: has_fuses("Activate Ziggurat Fuse", state, world) and has_ability(prayer, state, world))
|
rule=lambda state: has_ability(prayer, state, world))
|
||||||
|
|
||||||
regions["Rooted Ziggurat Portal Room"].connect(
|
regions["Rooted Ziggurat Portal Room"].connect(
|
||||||
connecting_region=regions["Rooted Ziggurat Portal Room Exit"],
|
connecting_region=regions["Rooted Ziggurat Portal Room Exit"],
|
||||||
|
|||||||
@@ -178,7 +178,7 @@ class WargrooveContext(CommonContext):
|
|||||||
self.remove_communication_files()
|
self.remove_communication_files()
|
||||||
atexit.register(self.remove_communication_files)
|
atexit.register(self.remove_communication_files)
|
||||||
if not os.path.isdir(appdata_wargroove):
|
if not os.path.isdir(appdata_wargroove):
|
||||||
print_error_and_close("WargrooveClient couldn't find Wargoove in appdata!"
|
print_error_and_close("WargrooveClient couldn't find Wargoove in appdata! "
|
||||||
"Boot Wargroove and then close it to attempt to fix this error")
|
"Boot Wargroove and then close it to attempt to fix this error")
|
||||||
mods_directory = os.path.join(appdata_wargroove, "mods", "ArchipelagoMod")
|
mods_directory = os.path.join(appdata_wargroove, "mods", "ArchipelagoMod")
|
||||||
save_directory = os.path.join(appdata_wargroove, "save")
|
save_directory = os.path.join(appdata_wargroove, "save")
|
||||||
|
|||||||
Reference in New Issue
Block a user