Compare commits

...

37 Commits

Author SHA1 Message Date
NewSoupVi
eaf352daaf MultiServer: Correct tying of Context.groups 2025-01-11 22:01:05 +01:00
Alchav
29b34ca9fd Pokémon R/B: Fix Route 11-E to Route-12-W logic (#4435) 2025-01-11 01:31:29 +01:00
Fabian Dill
d97ee5d209 Core: update certifi (#4453) 2025-01-10 23:28:57 +01:00
Fabian Dill
c2bd9df0f7 Subnautica: fix typo and remove no longer used logger (#4456) 2025-01-10 23:28:38 +01:00
Scipio Wright
112bfe0933 TUNIC: Logic for Beneath the Vault Bridge Switch #4432 2025-01-10 22:48:15 +01:00
Alchav
96b500679d LTTP: Add missing GT Pre-Moldorm Bomb Wall Logic (#4440) 2025-01-10 22:40:50 +01:00
Scipio Wright
258ea10c52 TUNIC: Modify UT support to make a better pattern (#3860)
* Modify UT support to make a better pattern

* Handle keyerror for logic_rules option

* Missed self.passthrough value setting

* Less laziness for passthrough

* Remove extra newline

* Fix missing using_ut = True, also remove now unnecessary try except since 0.5.1 is out

* New UT thing, it goes in this PR because it's been open for 5 months for a very very tiny change
2025-01-10 21:49:13 +01:00
lordlou
043ba418ec SM generate without rom (#3460)
* - SM now displays message when getting an item outside for someone else (fills ROM item table)

This is dependant on modifications done to sm_randomizer_rom project

* First working MultiWorld SM

* some missing things:

- player name inject in ROM and get in client
- end game get from ROM in client
- send self item to server
- add player names table in ROM

* replaced CollectionState inheritance from SMBoolManager with a composition of an array of it (required to generation more than one SM world, which is still fails but is better)

* - reenabled balancing

* post rebase fixes

* updated SmClient.py

* + added VariaRandomizer LICENSE

* + added sm_randomizer_rom project (which builds sm.ips)

* Moved VariaRandomizer and sm_randomizer_rom projects inside worlds/sm and done some cleaning

* properly revert change made to CollectionState and more cleaning

* Fixed multiworld support patch not working with VariaRandomizer's

* missing file commit

* Fixed syntax error in unused code to satisfy Linter

* Revert "Fixed multiworld support patch not working with VariaRandomizer's"

This reverts commit fb3ca18528bb331995e3d3051648c8f84d04c08b.

* many fixes and improovement

- fixed seeded generation
- fixed broken logic when more than one SM world
- added missing rules for inter-area transitions
- added basic patch presence for logic
- added DoorManager init call to reflect present patches for logic
- moved CollectionState addition out of BaseClasses into SM world
- added condition to apply progitempool presorting only if SM world is present
- set Bosses item id to None to prevent them going into multidata
- now use get_game_players

* first working (most of the time) progression generation for SM using VariaRandomizer's rules, items, locations and accessPoint (as regions)

* first working single-world randomized SM rom patches

* - SM now displays message when getting an item outside for someone else (fills ROM item table)

This is dependant on modifications done to sm_randomizer_rom project

* First working MultiWorld SM

* some missing things:

- player name inject in ROM and get in client
- end game get from ROM in client
- send self item to server
- add player names table in ROM

* replaced CollectionState inheritance from SMBoolManager with a composition of an array of it (required to generation more than one SM world, which is still fails but is better)

* - reenabled balancing

* post rebase fixes

* updated SmClient.py

* + added VariaRandomizer LICENSE

* + added sm_randomizer_rom project (which builds sm.ips)

* Moved VariaRandomizer and sm_randomizer_rom projects inside worlds/sm and done some cleaning

* properly revert change made to CollectionState and more cleaning

* Fixed multiworld support patch not working with VariaRandomizer's

* missing file commit

* Fixed syntax error in unused code to satisfy Linter

* Revert "Fixed multiworld support patch not working with VariaRandomizer's"

This reverts commit fb3ca18528bb331995e3d3051648c8f84d04c08b.

* many fixes and improovement

- fixed seeded generation
- fixed broken logic when more than one SM world
- added missing rules for inter-area transitions
- added basic patch presence for logic
- added DoorManager init call to reflect present patches for logic
- moved CollectionState addition out of BaseClasses into SM world
- added condition to apply progitempool presorting only if SM world is present
- set Bosses item id to None to prevent them going into multidata
- now use get_game_players

* Fixed multiworld support patch not working with VariaRandomizer's

Added stage_fill_hook to set morph first in progitempool
Added back VariaRandomizer's standard patches

* + added missing files from variaRandomizer project

* + added missing variaRandomizer files (custom sprites)

+ started integrating VariaRandomizer options (WIP)

* Some fixes for player and server name display

- fixed player name of 16 characters reading too far in SM client
- fixed 12 bytes SM player name limit (now 16)
- fixed server name not being displayed in SM when using server cheat ( now displays RECEIVED FROM ARCHIPELAGO)
- request: temporarly changed default seed names displayed in SM main menu to OWTCH

* Fixed Goal completion not triggering in smClient

* integrated VariaRandomizer's options into AP (WIP)

- startAP is working
- door rando is working
- skillset is working

* - fixed itemsounds.ips crash by always including nofanfare.ips into multiworld.ips (itemsounds is now always applied and "itemsounds" preset must always be "off")

* skillset are now instanced per player instead of being a singleton class

* RomPatches are now instanced per player instead of being a singleton class

* DoorManager is now instanced per player instead of being a singleton class

* - fixed the last bugs that prevented generation of >1 SM world

* fixed crash when no skillset preset is specified in randoPreset (default to "casual")

* maxDifficulty support and itemsounds removal

- added support for maxDifficulty
- removed itemsounds patch as its always applied from multiworld patch for now

* Fixed bad merge

* Post merge adaptation

* fixed player name length fix that got lost with the merge

* fixed generation with other game type than SM

* added default randoPreset json for SM in playerSettings.yaml

* fixed broken SM client following merge

* beautified json skillset presets

* Fixed ArchipelagoSmClient not building

* Fixed conflict between mutliworld patch and beam_doors_plms patch

- doorsColorsRando now working

* SM generation now outputs APBP

- Fixed paths for patches and presets when frozen

* added missing file and fixed multithreading issue

* temporarily set data_version = 0

* more work

- added support for AP starting items
- fixed client crash with gamemode being None
- patch.py "compatible_version" is now 3

* commited missing asm files

fixed start item reserve breaking game (was using bad write offset when patching)

* Nothing item are now handled game-side. the game will now skip displaying a message box for received Nothing item (but the client will still receive it).

fixed crash in SMClient when loosing connection to SNI

* fixed No Energy Item missing its ID

fixed Plando

* merge post fixes

* fixed start item Grapple, XRay and Reserve HUD, as well as graphic beams (except ice palette color)

* fixed freeze in blue brinstar caused by Varia's custom PLM not being filled with proper Multiworld PLM address (altLocsAddresses)

* fixed start item x-ray HUD display

* Fixed start items being sent by the server (is all handled in ROM)

Start items are now not removed from itempool anymore
Nothing Item is now local_items so no player will ever pickup Nothing. Doing so reduces contribution of this world to the Multiworld the more Nothing there is though.
Fixed crash (and possibly passing but broken) at generation where the static list of IPSPatches used by all SM worlds was being modified

* fixed settings that could be applied to any SM players

* fixed auth to server only using player name (now does as ALTTP to authenticate)

* - fixed End Credits broken text

* added non SM item name display

* added all supported SM options in playerSettings.yaml

* fixed locations needing a list of parent regions (now generate a region for each location with one-way exits to each (previously) parent region

did some cleaning (mainly reverts on unnecessary core classes

* minor setting fixes and tweaks

- merged Area and lightArea settings
- made missileQty, superQty and powerBombQty use value from 10 to 90 and divide value by float(10) when generating
- fixed inverted layoutPatch setting

* added option start_inventory_removes_from_pool

fixed option names formatting
fixed lint errors
small code and repo cleanup

* Hopefully fixed ROR2 that could not send any items

* - fixed missing required change to ROR2

* fixed 0 hp when respawning without having ever saved (start items were not updating the save checksum)

* fixed typo with doors_colors_rando

* fixed checksum

* added custom sprites for off-world items (progression or not)

the original AP sprite was made with PierRoulette's SM Item Sprite Utility by ijwu

* - added missing change following upstream merge

- changed patch filename extension from apbp to apm3 so patch can be used with the new client

* added morph placement options: early means local and sphere 1

* fixed failing unit tests

* - fixed broken custom_preset options

* - big cleanup to remove unnecessary or unsupported features

* - more cleanup

* - moved sm_randomizer_rom and all always applied patches into an external project that outputs basepatch.ips

- small cleanup

* - added comment to refer to project for generating basepatch.ips (https://github.com/lordlou/SMBasepatch)

* fixed g4_skip patch that can be not applied if hud is enabled

* - fixed off world sprite that can have broken graphics (restricted to use only first 2 palette)

* - updated basepatch to reflect g4_skip removal

- moved more asm files to SMBasepatch project

* - tourian grey doors at baby metroid are now always flashing (allowing to go back if needed)

* fixed wrong path if using built as exe

* - cleaned exposed maxDifficulty options

- removed always enabled Knows

* Merged LttPClient and SMClient into SNIClient

* added varia_custom Preset Option that fetch a preset (read from a new varia_custom_preset Option) from varia's web service

* small doc precision

* - added death_link support

- fixed broken Goal Completion
- post merge fix

* - removed now useless presets

* - fixed bad internal mapping with maxDiff

- increases maxDiff if only Bosses is preventing beating the game

* - added support for lowercase custom preset sections (knows, settings and controller)

- fixed controller settings not applying to ROM

* - fixed death loop when dying with Door rando, bomb or speed booster as starting items

- varia's backup save should now be usable (automatically enabled when doing door rando)

* -added docstring for generated yaml

* fixed bad merge

* fixed broken infinity max difficulty

* commented debug prints

* adjusted credits to mark progression speed and difficulty as Non Available

* added support for more than 255 players (will print Archipelago for higher player number)

* fixed missing cleanup

* added support for 65535 different player names in ROM

* fixed generations failing when only bosses are unreachable

* - replaced setting maxDiff to infinity with a bool only affecting boss logics if only bosses are left to finish

* fixed failling generations when using 'fun' settings

Accessibility checks are forced to 'items' if restricted locations are used by VARIA following usage of 'fun' settings

* fixed debug logger

* removed unsupported "suits_restriction" option

* fixed generations failing when only bosses are unreachable (using a less intrusive approach for AP)

* - fixed deathlink emptying reserves

- added death_link_survive option that lets player survive when receiving a deathlink if the have non-empty reserves

* - merged death_link and death_link_survive options

* fixed death_link

* added a fallback default starting location instead of failing generation if an invalid one was chosen

* added Nothing and NoEnergy as hint blacklist

added missing NoEnergy as local items and removed it from progression

* SM Varia can now generate without ROM

* removed stage_assert_generate
2025-01-10 21:46:17 +01:00
Fabian Dill
894a8571ee kvui: add autocompleting new hint text input (#3535)
Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
Co-authored-by: Silvris <58583688+Silvris@users.noreply.github.com>
2025-01-10 20:21:02 +01:00
ruby0b
874197d940 Linux: move the user home Archipelago dir to $XDG_DATA_HOME (#4347)
This affects builds with non-writable installation directories.
Instead of saving data in ~/Archipelago we now use $XDG_DATA_HOME/Archipelago
(defaulting to ~/.local/share/Archipelago).
If ~/Archipelago still exists we move it to the new location and link ~/Archipelago to it.

Motivation: This follows the [XDG Base Directory Specification](https://specifications.freedesktop.org/basedir-spec/latest/)
to at least some degree and doesn't clutter the user's home directory.
2025-01-10 01:27:49 +01:00
agilbert1412
d3ed40cd4d Stardew Valley: Hide the Mods from the simple options page (#4446) 2025-01-08 08:13:32 +01:00
Aaron Wagener
a29ba4a6c4 The Messenger: reduce strictness of output path check (#4442) 2025-01-07 23:11:26 +01:00
Fabian Dill
fe06fe075e Factorio: add fluid mining technology to logic requirements (#4385) 2025-01-07 23:06:48 +01:00
qwint
de58cb03da Core: Pickle hints by value (#4441) 2025-01-07 22:24:19 +01:00
TheLX5
3204680662 SNIClient: Let clients based on SNIClient monitor packages via on_package method (#3093) 2025-01-07 00:10:23 +01:00
shananas
07e896508c KH2: Doc Updates (#4434) 2025-01-06 14:02:04 -05:00
Scipio Wright
2d3faea713 Core: Include unfilled locations in error when there are not enough locations for progression items (#4285)
Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
2025-01-06 09:52:33 -05:00
eudaimonistic
7c89a83d19 Docs: Clarify !alias commands in commands_en.md (#4426)
Co-authored-by: Aaron Wagener <mmmcheese158@gmail.com>
2025-01-06 09:42:18 -05:00
qwint
16f8b41cb9 Core: add docstrings for launcher components (#4148)
Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
2025-01-06 09:35:37 -05:00
qwint
7d506990f5 HK: add location counts to option descriptions (#4083) 2025-01-06 09:35:12 -05:00
qwint
aadcb4c903 HK: use rich_text_options_doc to make webhost formatting look better (#4079) 2025-01-06 09:21:44 -05:00
coveleski
daf94fcdb2 Pokemon RB: Fixing misnamed locations (#4404) 2025-01-04 08:27:41 -05:00
Kory Dondzila
1cef659b78 Shivers: Fix spelling error in naming (#4425) 2025-01-04 07:42:34 -05:00
Scipio Wright
25381ef2c2 Core: Make the error for a missing option display the player name (#4430) 2025-01-04 07:29:30 -05:00
Mysteryem
5927926314 Blasphemous: Fix starting_location: random affecting all Blasphemous worlds (#4428)
Option resolution for the `StartingLocation` option (the only
`ChoiceIsRandom` subclass) was writing to the `randomized` attribute on
the class instead of on the instance, meaning that
`self.options.starting_location.randomized` would be `True` for all
Blasphemous players in the multiworld if any one of the players set
their `StartingLocation` option to `"random"`.

This patch fixes the issue by writing to the `randomized` attribute on
the new instance instead of on the class.
2025-01-03 07:03:30 -05:00
CaitSith2
2a11d9fec3 try again to award the starting items post cutscene if needed. (#4408) 2025-01-02 19:45:32 -08:00
Nicholas Saylor
82c44aaa22 FFMQ: Fix encoding issue with Game Page (#4299) 2025-01-02 22:03:07 -05:00
Kory Dondzila
a7b483e4b7 Shivers: Adds ixupi captures priority option (#4403) 2025-01-02 10:12:00 -05:00
Fabian Dill
917335ec54 Core: it's 2025 (#4417) 2025-01-01 02:02:18 +01:00
Mysteryem
6e59ee2926 Zork Grand Inquisitor: Precollect Start with Hotspot Items in deterministic order (#4412) 2024-12-31 09:16:29 -05:00
Mysteryem
3c9270d802 FFMQ: Create itempool in deterministic order (#4413) 2024-12-31 09:02:02 -05:00
Mysteryem
c4bbcf9890 TUNIC: Add relics and abilities to the item pool in deterministic order (#4411) 2024-12-30 23:57:09 -05:00
NewSoupVi
8dbecf3d57 The Witness: Make location order in the spoiler log deterministic (#3895)
* Fix location order

* Update worlds/witness/data/static_logic.py

Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>

---------

Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
2024-12-30 00:50:39 +01:00
Fabian Dill
0de1369ec5 Factorio: hide hidden vanilla techs in factoriopedia too (#4332) 2024-12-29 11:56:41 -08:00
Fabian Dill
fa95ae4b24 Factorio: require version that fixes a randomizer exploit (#4391) 2024-12-29 11:55:40 -08:00
CaitSith2
2065246186 Factorio: Make it possible to use rocket part in blueprint parameterization. (#4396)
This allows for example, making a blueprint of your rocket silo with requester chests specifying a request for the 2-8 rocket part ingredients needed to build the rocket.
2024-12-29 20:13:34 +01:00
Kory Dondzila
ca1b3df45b Shivers: Follow on PR to cleanup options #4401 2024-12-27 23:38:01 +01:00
49 changed files with 529 additions and 251 deletions

View File

@@ -531,7 +531,8 @@ def distribute_items_restrictive(multiworld: MultiWorld,
if progitempool:
raise FillError(
f"Not enough locations for progression items. "
f"There are {len(progitempool)} more progression items than there are available locations.",
f"There are {len(progitempool)} more progression items than there are available locations.\n"
f"Unfilled locations:\n{multiworld.get_unfilled_locations()}.",
multiworld=multiworld,
)
accessibility_corrections(multiworld, multiworld.state, defaultlocations)

View File

@@ -500,7 +500,8 @@ def roll_settings(weights: dict, plando_options: PlandoOptions = PlandoOptions.b
for option_key in game_weights:
if option_key in {"triggers", *valid_keys}:
continue
logging.warning(f"{option_key} is not a valid option name for {ret.game} and is not present in triggers.")
logging.warning(f"{option_key} is not a valid option name for {ret.game} and is not present in triggers "
f"for player {ret.name}.")
if PlandoOptions.items in plando_options:
ret.plando_items = copy.deepcopy(game_weights.get("plando_items", []))
if ret.game == "A Link to the Past":

View File

@@ -1,7 +1,7 @@
MIT License
Copyright (c) 2017 LLCoolDave
Copyright (c) 2022 Berserker66
Copyright (c) 2025 Berserker66
Copyright (c) 2022 CaitSith2
Copyright (c) 2021 LegendaryLinux

View File

@@ -444,7 +444,7 @@ class Context:
self.slot_info = decoded_obj["slot_info"]
self.games = {slot: slot_info.game for slot, slot_info in self.slot_info.items()}
self.groups = {slot: slot_info.group_members for slot, slot_info in self.slot_info.items()
self.groups = {slot: set(slot_info.group_members) for slot, slot_info in self.slot_info.items()
if slot_info.type == SlotType.group}
self.clients = {0: {}}

View File

@@ -10,7 +10,7 @@ import websockets
from Utils import ByValue, Version
class HintStatus(enum.IntEnum):
class HintStatus(ByValue, enum.IntEnum):
HINT_FOUND = 0
HINT_UNSPECIFIED = 1
HINT_NO_PRIORITY = 10

View File

@@ -243,6 +243,9 @@ class SNIContext(CommonContext):
# Once the games handled by SNIClient gets made to be remote items,
# this will no longer be needed.
async_start(self.send_msgs([{"cmd": "LocationScouts", "locations": list(new_locations)}]))
if self.client_handler is not None:
self.client_handler.on_package(self, cmd, args)
def run_gui(self) -> None:
from kvui import GameManager

View File

@@ -152,8 +152,15 @@ def home_path(*path: str) -> str:
if hasattr(home_path, 'cached_path'):
pass
elif sys.platform.startswith('linux'):
home_path.cached_path = os.path.expanduser('~/Archipelago')
os.makedirs(home_path.cached_path, 0o700, exist_ok=True)
xdg_data_home = os.getenv('XDG_DATA_HOME', os.path.expanduser('~/.local/share'))
home_path.cached_path = xdg_data_home + '/Archipelago'
if not os.path.isdir(home_path.cached_path):
legacy_home_path = os.path.expanduser('~/Archipelago')
if os.path.isdir(legacy_home_path):
os.renames(legacy_home_path, home_path.cached_path)
os.symlink(home_path.cached_path, legacy_home_path)
else:
os.makedirs(home_path.cached_path, 0o700, exist_ok=True)
else:
# not implemented
home_path.cached_path = local_path() # this will generate the same exceptions we got previously

View File

@@ -1,6 +1,6 @@
{% block footer %}
<footer id="island-footer">
<div id="copyright-notice">Copyright 2024 Archipelago</div>
<div id="copyright-notice">Copyright 2025 Archipelago</div>
<div id="links">
<a href="/sitemap">Site Map</a>
-

View File

@@ -147,3 +147,8 @@
rectangle: self.x-2, self.y-2, self.width+4, self.height+4
<ServerToolTip>:
pos_hint: {'center_y': 0.5, 'center_x': 0.5}
<AutocompleteHintInput>
size_hint_y: None
height: dp(30)
multiline: False
write_tab: False

65
kvui.py
View File

@@ -40,7 +40,7 @@ from kivy.core.image import ImageLoader, ImageLoaderBase, ImageData
from kivy.base import ExceptionHandler, ExceptionManager
from kivy.clock import Clock
from kivy.factory import Factory
from kivy.properties import BooleanProperty, ObjectProperty
from kivy.properties import BooleanProperty, ObjectProperty, NumericProperty
from kivy.metrics import dp
from kivy.effects.scroll import ScrollEffect
from kivy.uix.widget import Widget
@@ -64,6 +64,7 @@ from kivy.uix.recycleboxlayout import RecycleBoxLayout
from kivy.uix.recycleview.layout import LayoutSelectionBehavior
from kivy.animation import Animation
from kivy.uix.popup import Popup
from kivy.uix.dropdown import DropDown
from kivy.uix.image import AsyncImage
fade_in_animation = Animation(opacity=0, duration=0) + Animation(opacity=1, duration=0.25)
@@ -305,6 +306,50 @@ class SelectableLabel(RecycleDataViewBehavior, TooltipLabel):
""" Respond to the selection of items in the view. """
self.selected = is_selected
class AutocompleteHintInput(TextInput):
min_chars = NumericProperty(3)
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.dropdown = DropDown()
self.dropdown.bind(on_select=lambda instance, x: setattr(self, 'text', x))
self.bind(on_text_validate=self.on_message)
def on_message(self, instance):
App.get_running_app().commandprocessor("!hint "+instance.text)
def on_text(self, instance, value):
if len(value) >= self.min_chars:
self.dropdown.clear_widgets()
ctx: context_type = App.get_running_app().ctx
if not ctx.game:
return
item_names = ctx.item_names._game_store[ctx.game].values()
def on_press(button: Button):
split_text = MarkupLabel(text=button.text).markup
return self.dropdown.select("".join(text_frag for text_frag in split_text
if not text_frag.startswith("[")))
lowered = value.lower()
for item_name in item_names:
try:
index = item_name.lower().index(lowered)
except ValueError:
pass # substring not found
else:
text = escape_markup(item_name)
text = text[:index] + "[b]" + text[index:index+len(value)]+"[/b]"+text[index+len(value):]
btn = Button(text=text, size_hint_y=None, height=dp(30), markup=True)
btn.bind(on_release=on_press)
self.dropdown.add_widget(btn)
if not self.dropdown.attach_to:
self.dropdown.open(self)
else:
self.dropdown.dismiss()
class HintLabel(RecycleDataViewBehavior, BoxLayout):
selected = BooleanProperty(False)
striped = BooleanProperty(False)
@@ -570,8 +615,10 @@ class GameManager(App):
# show Archipelago tab if other logging is present
self.tabs.add_widget(panel)
hint_panel = self.add_client_tab("Hints", HintLog(self.json_to_kivy_parser))
hint_panel = self.add_client_tab("Hints", HintLayout())
self.hint_log = HintLog(self.json_to_kivy_parser)
self.log_panels["Hints"] = hint_panel.content
hint_panel.content.add_widget(self.hint_log)
if len(self.logging_pairs) == 1:
self.tabs.default_tab_text = "Archipelago"
@@ -698,7 +745,7 @@ class GameManager(App):
def update_hints(self):
hints = self.ctx.stored_data.get(f"_read_hints_{self.ctx.team}_{self.ctx.slot}", [])
self.log_panels["Hints"].refresh_hints(hints)
self.hint_log.refresh_hints(hints)
# default F1 keybind, opens a settings menu, that seems to break the layout engine once closed
def open_settings(self, *largs):
@@ -753,6 +800,17 @@ class UILog(RecycleView):
element.height = element.texture_size[1]
class HintLayout(BoxLayout):
orientation = "vertical"
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
boxlayout = BoxLayout(orientation="horizontal", size_hint_y=None, height=dp(30))
boxlayout.add_widget(Label(text="New Hint:", size_hint_x=None, size_hint_y=None, height=dp(30)))
boxlayout.add_widget(AutocompleteHintInput())
self.add_widget(boxlayout)
status_names: typing.Dict[HintStatus, str] = {
HintStatus.HINT_FOUND: "Found",
HintStatus.HINT_UNSPECIFIED: "Unspecified",
@@ -769,6 +827,7 @@ status_colors: typing.Dict[HintStatus, str] = {
}
class HintLog(RecycleView):
header = {
"receiving": {"text": "[u]Receiving Player[/u]"},

View File

@@ -7,7 +7,7 @@ schema>=0.7.7
kivy>=2.3.0
bsdiff4>=1.2.4
platformdirs>=4.2.2
certifi>=2024.8.30
certifi>=2024.12.14
cython>=3.0.11
cymem>=2.0.8
orjson>=3.10.7

View File

@@ -86,3 +86,7 @@ class SNIClient(abc.ABC, metaclass=AutoSNIClientRegister):
async def deathlink_kill_player(self, ctx: SNIContext) -> None:
""" override this with implementation to kill player """
pass
def on_package(self, ctx: SNIContext, cmd: str, args: Dict[str, Any]) -> None:
""" override this with code to handle packages from the server """
pass

View File

@@ -18,16 +18,42 @@ class Type(Enum):
class Component:
"""
A Component represents a process launchable by Archipelago Launcher, either by a User action in the GUI,
by resolving an archipelago://user:pass@host:port link from the WebHost, by resolving a patch file's metadata,
or by using a component name arg while running the Launcher in CLI i.e. `ArchipelagoLauncher.exe "Text Client"`
Expected to be appended to LauncherComponents.component list to be used.
"""
display_name: str
"""Used as the GUI button label and the component name in the CLI args"""
type: Type
"""
Enum "Type" classification of component intent, for filtering in the Launcher GUI
If not set in the constructor, it will be inferred by display_name
"""
script_name: Optional[str]
"""Recommended to use func instead; Name of file to run when the component is called"""
frozen_name: Optional[str]
"""Recommended to use func instead; Name of the frozen executable file for this component"""
icon: str # just the name, no suffix
"""Lookup ID for the icon path in LauncherComponents.icon_paths"""
cli: bool
"""Bool to control if the component gets launched in an appropriate Terminal for the OS"""
func: Optional[Callable]
"""
Function that gets called when the component gets launched
Any arg besides the component name arg is passed into the func as well, so handling *args is suggested
"""
file_identifier: Optional[Callable[[str], bool]]
"""
Function that is run against patch file arg to identify which component is appropriate to launch
If the function is an Instance of SuffixIdentifier the suffixes will also be valid for the Open Patch component
"""
game_name: Optional[str]
"""Game name to identify component when handling launch links from WebHost"""
supports_uri: Optional[bool]
"""Bool to identify if a component supports being launched by launch links from WebHost"""
def __init__(self, display_name: str, script_name: Optional[str] = None, frozen_name: Optional[str] = None,
cli: bool = False, icon: str = 'icon', component_type: Optional[Type] = None,

View File

@@ -592,9 +592,9 @@ def global_rules(multiworld: MultiWorld, player: int):
lambda state: can_kill_most_things(state, player, 8) and has_fire_source(state, player) and state.multiworld.get_entrance('Ganons Tower Torch Rooms', player).parent_region.dungeon.bosses['middle'].can_defeat(state))
set_rule(multiworld.get_location('Ganons Tower - Mini Helmasaur Key Drop', player), lambda state: can_kill_most_things(state, player, 1))
set_rule(multiworld.get_location('Ganons Tower - Pre-Moldorm Chest', player),
lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 7))
lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 7) and can_use_bombs(state, player))
set_rule(multiworld.get_entrance('Ganons Tower Moldorm Door', player),
lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 8))
lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 8) and can_use_bombs(state, player))
set_rule(multiworld.get_entrance('Ganons Tower Moldorm Gap', player),
lambda state: state.has('Hookshot', player) and state.multiworld.get_entrance('Ganons Tower Moldorm Gap', player).parent_region.dungeon.bosses['top'].can_defeat(state))
set_defeat_dungeon_boss_rule(multiworld.get_location('Agahnim 2', player))

View File

@@ -130,19 +130,21 @@ class TestGanonsTower(TestDungeon):
["Ganons Tower - Pre-Moldorm Chest", False, []],
["Ganons Tower - Pre-Moldorm Chest", False, [], ['Progressive Bow']],
["Ganons Tower - Pre-Moldorm Chest", False, [], ['Bomb Upgrade (50)']],
["Ganons Tower - Pre-Moldorm Chest", False, [], ['Big Key (Ganons Tower)']],
["Ganons Tower - Pre-Moldorm Chest", False, [], ['Lamp', 'Fire Rod']],
["Ganons Tower - Pre-Moldorm Chest", True, ['Progressive Bow', 'Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Lamp']],
["Ganons Tower - Pre-Moldorm Chest", True, ['Progressive Bow', 'Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Fire Rod']],
["Ganons Tower - Pre-Moldorm Chest", True, ['Bomb Upgrade (50)', 'Progressive Bow', 'Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Lamp']],
["Ganons Tower - Pre-Moldorm Chest", True, ['Bomb Upgrade (50)', 'Progressive Bow', 'Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Fire Rod']],
["Ganons Tower - Validation Chest", False, []],
["Ganons Tower - Validation Chest", False, [], ['Hookshot']],
["Ganons Tower - Validation Chest", False, [], ['Progressive Bow']],
["Ganons Tower - Validation Chest", False, [], ['Bomb Upgrade (50)']],
["Ganons Tower - Validation Chest", False, [], ['Big Key (Ganons Tower)']],
["Ganons Tower - Validation Chest", False, [], ['Lamp', 'Fire Rod']],
["Ganons Tower - Validation Chest", False, [], ['Progressive Sword', 'Hammer']],
["Ganons Tower - Validation Chest", True, ['Progressive Bow', 'Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Lamp', 'Hookshot', 'Progressive Sword']],
["Ganons Tower - Validation Chest", True, ['Progressive Bow', 'Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Fire Rod', 'Hookshot', 'Progressive Sword']],
["Ganons Tower - Validation Chest", True, ['Progressive Bow', 'Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Lamp', 'Hookshot', 'Hammer']],
["Ganons Tower - Validation Chest", True, ['Progressive Bow', 'Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Fire Rod', 'Hookshot', 'Hammer']],
["Ganons Tower - Validation Chest", True, ['Bomb Upgrade (50)', 'Progressive Bow', 'Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Lamp', 'Hookshot', 'Progressive Sword']],
["Ganons Tower - Validation Chest", True, ['Bomb Upgrade (50)', 'Progressive Bow', 'Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Fire Rod', 'Hookshot', 'Progressive Sword']],
["Ganons Tower - Validation Chest", True, ['Bomb Upgrade (50)', 'Progressive Bow', 'Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Lamp', 'Hookshot', 'Hammer']],
["Ganons Tower - Validation Chest", True, ['Bomb Upgrade (50)', 'Progressive Bow', 'Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Fire Rod', 'Hookshot', 'Hammer']],
])

View File

@@ -4,14 +4,17 @@ import random
class ChoiceIsRandom(Choice):
randomized: bool = False
randomized: bool
def __init__(self, value: int, randomized: bool = False):
super().__init__(value)
self.randomized = randomized
@classmethod
def from_text(cls, text: str) -> Choice:
text = text.lower()
if text == "random":
cls.randomized = True
return cls(random.choice(list(cls.name_lookup)))
return cls(random.choice(list(cls.name_lookup)), True)
for option_name, value in cls.options.items():
if option_name == text:
return cls(value)

View File

@@ -37,8 +37,8 @@ base_info = {
"description": "Integration client for the Archipelago Randomizer",
"factorio_version": "2.0",
"dependencies": [
"base >= 2.0.15",
"? quality >= 2.0.15",
"base >= 2.0.28",
"? quality >= 2.0.28",
"! space-age",
"? science-not-invited",
"? factory-levels"

View File

@@ -63,17 +63,19 @@ class FactorioElement:
class Technology(FactorioElement): # maybe make subclass of Location?
has_modifier: bool
factorio_id: int
progressive: Tuple[str]
unlocks: Union[Set[str], bool] # bool case is for progressive technologies
modifiers: list[str]
def __init__(self, technology_name: str, factorio_id: int, progressive: Tuple[str] = (),
has_modifier: bool = False, unlocks: Union[Set[str], bool] = None):
modifiers: list[str] = None, unlocks: Union[Set[str], bool] = None):
self.name = technology_name
self.factorio_id = factorio_id
self.progressive = progressive
self.has_modifier = has_modifier
if modifiers is None:
modifiers = []
self.modifiers = modifiers
if unlocks:
self.unlocks = unlocks
else:
@@ -82,6 +84,10 @@ class Technology(FactorioElement): # maybe make subclass of Location?
def __hash__(self):
return self.factorio_id
@property
def has_modifier(self) -> bool:
return bool(self.modifiers)
def get_custom(self, world, allowed_packs: Set[str], player: int) -> CustomTechnology:
return CustomTechnology(self, world, allowed_packs, player)
@@ -191,13 +197,14 @@ class Machine(FactorioElement):
recipe_sources: Dict[str, Set[str]] = {} # recipe_name -> technology source
mining_with_fluid_sources: set[str] = set()
# recipes and technologies can share names in Factorio
for technology_name, data in sorted(techs_future.result().items()):
technology = Technology(
technology_name,
factorio_tech_id,
has_modifier=data["has_modifier"],
modifiers=data.get("modifiers", []),
unlocks=set(data["unlocks"]) - start_unlocked_recipes,
)
factorio_tech_id += 1
@@ -205,7 +212,8 @@ for technology_name, data in sorted(techs_future.result().items()):
technology_table[technology_name] = technology
for recipe_name in technology.unlocks:
recipe_sources.setdefault(recipe_name, set()).add(technology_name)
if "mining-with-fluid" in technology.modifiers:
mining_with_fluid_sources.add(technology_name)
del techs_future
recipes = {}
@@ -221,6 +229,8 @@ for resource_name, resource_data in resources_future.result().items():
"energy": resource_data["mining_time"],
"category": resource_data["category"]
}
if "required_fluid" in resource_data:
recipe_sources.setdefault(f"mining-{resource_name}", set()).update(mining_with_fluid_sources)
del resources_future
for recipe_name, recipe_data in raw_recipes.items():
@@ -431,7 +441,9 @@ for root in sorted_rows:
factorio_tech_id += 1
progressive_technology = Technology(root, factorio_tech_id,
tuple(progressive),
has_modifier=any(technology_table[tech].has_modifier for tech in progressive),
modifiers=sorted(set.union(
*(set(technology_table[tech].modifiers) for tech in progressive)
)),
unlocks=any(technology_table[tech].unlocks for tech in progressive),)
progressive_tech_table[root] = progressive_technology.factorio_id
progressive_technology_table[root] = progressive_technology

View File

@@ -445,6 +445,10 @@ end
script.on_event(defines.events.on_player_main_inventory_changed, update_player_event)
-- Update players when the cutscene is cancelled or finished. (needed for skins_factored)
script.on_event(defines.events.on_cutscene_cancelled, update_player_event)
script.on_event(defines.events.on_cutscene_finished, update_player_event)
function add_samples(force, name, count)
local function add_to_table(t)
if count <= 0 then

View File

@@ -1,6 +1,7 @@
{% from "macros.lua" import dict_to_recipe, variable_to_lua %}
-- this file gets written automatically by the Archipelago Randomizer and is in its raw form a Jinja2 Template
require('lib')
data.raw["item"]["rocket-part"].hidden = false
data.raw["rocket-silo"]["rocket-silo"].fluid_boxes = {
{
production_type = "input",
@@ -162,6 +163,7 @@ data.raw["ammo"]["artillery-shell"].stack_size = 10
{# each randomized tech gets set to be invisible, with new nodes added that trigger those #}
{%- for original_tech_name in base_tech_table -%}
technologies["{{ original_tech_name }}"].hidden = true
technologies["{{ original_tech_name }}"].hidden_in_factoriopedia = true
{% endfor %}
{%- for location, item in locations %}
{#- the tech researched by the local player #}

File diff suppressed because one or more lines are too long

View File

@@ -260,7 +260,8 @@ def create_items(self) -> None:
items.append(i)
for item_group in ("Key Items", "Spells", "Armors", "Helms", "Shields", "Accessories", "Weapons"):
for item in self.item_name_groups[item_group]:
# Sort for deterministic order
for item in sorted(self.item_name_groups[item_group]):
add_item(item)
if self.options.brown_boxes == "include":

View File

@@ -1,7 +1,7 @@
# Final Fantasy Mystic Quest
## Game page in other languages:
* [Français](/games/Final%20Fantasy%20Mystic%20Quest/info/fr)
* [Français](/games/Final%20Fantasy%20Mystic%20Quest/info/fr)
## Where is the options page?

View File

@@ -27,6 +27,7 @@ including the exclamation point.
- `!countdown <number of seconds>` Starts a countdown using the given seconds value. Useful for synchronizing starts.
Defaults to 10 seconds if no argument is provided.
- `!alias <alias>` Sets your alias, which allows you to use commands with the alias rather than your provided name.
`!alias` on its own will reset the alias to the player's original name.
- `!admin <command>` Executes a command as if you typed it into the server console. Remote administration must be
enabled.
@@ -65,6 +66,7 @@ including the exclamation point.
argument is provided.
- `/option <option name> <option value>` Set a server option. For a list of options, use the `/options` command.
- `/alias <player name> <alias name>` Assign a player an alias, allowing you to reference the player by the alias in commands.
`!alias <player name>` on its own will reset the alias to the player's original name.
### Collect/Release

View File

@@ -132,7 +132,13 @@ splitter_pattern = re.compile(r'(?<!^)(?=[A-Z])')
for option_name, option_data in pool_options.items():
extra_data = {"__module__": __name__, "items": option_data[0], "locations": option_data[1]}
if option_name in option_docstrings:
extra_data["__doc__"] = option_docstrings[option_name]
if option_name == "RandomizeFocus":
# pool options for focus are just lying
count = 1
else:
count = len([loc for loc in option_data[1] if loc != "Start"])
extra_data["__doc__"] = option_docstrings[option_name] + \
f"\n This option adds approximately {count} location{'s' if count != 1 else ''}."
if option_name in default_on:
option = type(option_name, (DefaultOnToggle,), extra_data)
else:
@@ -213,6 +219,7 @@ class MaximumEssencePrice(MinimumEssencePrice):
class MinimumEggPrice(Range):
"""The minimum rancid egg price in the range of prices that an item should cost from Jiji.
Only takes effect if the EggSlotShops option is greater than 0."""
rich_text_doc = False
display_name = "Minimum Egg Price"
range_start = 1
range_end = 20
@@ -222,6 +229,7 @@ class MinimumEggPrice(Range):
class MaximumEggPrice(MinimumEggPrice):
"""The maximum rancid egg price in the range of prices that an item should cost from Jiji.
Only takes effect if the EggSlotShops option is greater than 0."""
rich_text_doc = False
display_name = "Maximum Egg Price"
default = 10
@@ -265,6 +273,7 @@ class RandomCharmCosts(NamedRange):
Set to -1 or vanilla for vanilla costs.
Set to -2 or shuffle to shuffle around the vanilla costs to different charms."""
rich_text_doc = False
display_name = "Randomize Charm Notch Costs"
range_start = 0
range_end = 240
@@ -437,6 +446,7 @@ class Goal(Choice):
class GrubHuntGoal(NamedRange):
"""The amount of grubs required to finish Grub Hunt.
On 'All' any grubs from item links replacements etc. will be counted"""
rich_text_doc = False
display_name = "Grub Hunt Goal"
range_start = 1
range_end = 46
@@ -446,7 +456,7 @@ class GrubHuntGoal(NamedRange):
class WhitePalace(Choice):
"""
Whether or not to include White Palace or not. Note: Even if excluded, the King Fragment check may still be
Whether or not to include White Palace or not. Note: Even if excluded, the King Fragment check may still be
required if charms are vanilla.
"""
display_name = "White Palace"
@@ -483,6 +493,7 @@ class DeathLinkShade(Choice):
** Self-death shade behavior is not changed; if a self-death normally creates a shade in vanilla, it will override
your existing shade, if any.
"""
rich_text_doc = False
option_vanilla = 0
option_shadeless = 1
option_shade = 2
@@ -497,6 +508,7 @@ class DeathLinkBreaksFragileCharms(Toggle):
** Self-death fragile charm behavior is not changed; if a self-death normally breaks fragile charms in vanilla, it
will continue to do so.
"""
rich_text_doc = False
display_name = "Deathlink Breaks Fragile Charms"
@@ -515,6 +527,7 @@ class CostSanity(Choice):
These costs can be in Geo (except Grubfather, Seer and Eggshop), Grubs, Charms, Essence and/or Rancid Eggs
"""
rich_text_doc = False
option_off = 0
alias_no = 0
option_on = 1

View File

@@ -134,7 +134,9 @@ shop_cost_types: typing.Dict[str, typing.Tuple[str, ...]] = {
class HKWeb(WebWorld):
setup_en = Tutorial(
rich_text_options_doc = True
setup_en = Tutorial(
"Mod Setup and Use Guide",
"A guide to playing Hollow Knight with Archipelago.",
"English",
@@ -143,7 +145,7 @@ class HKWeb(WebWorld):
["Ijwu"]
)
setup_pt_br = Tutorial(
setup_pt_br = Tutorial(
setup_en.tutorial_name,
setup_en.description,
"Português Brasileiro",

View File

@@ -7,18 +7,21 @@
<h2 style="text-transform:none";>Required Software:</h2>
`Kingdom Hearts II Final Mix` from the [Epic Games Store](https://store.epicgames.com/en-US/discover/kingdom-hearts) or [Steam](https://store.steampowered.com/app/2552430/KINGDOM_HEARTS_HD_1525_ReMIX/)
Kingdom Hearts II Final Mix from the [Epic Games Store](https://store.epicgames.com/en-US/discover/kingdom-hearts) or [Steam](https://store.steampowered.com/app/2552430/KINGDOM_HEARTS_HD_1525_ReMIX/)
- Follow this Guide to set up these requirements [KH2Rando.com](https://tommadness.github.io/KH2Randomizer/setup/Panacea-ModLoader/)
1. `Version 3.3.0 or greater OpenKH Mod Manager with Panacea`
2. `Lua Backend from the OpenKH Mod Manager`
3. `Install the mod KH2FM-Mods-Num/GoA-ROM-Edition using OpenKH Mod Manager`
1. Version 3.4.0 or greater OpenKH Mod Manager with Panacea
2. Lua Backend from the OpenKH Mod Manager
3. Install the mod `KH2FM-Mods-Num/GoA-ROM-Edition` using OpenKH Mod Manager
- Needed for Archipelago
1. [`ArchipelagoKH2Client.exe`](https://github.com/ArchipelagoMW/Archipelago/releases)
2. `Install the Archipelago Companion mod from JaredWeakStrike/APCompanion using OpenKH Mod Manager`
3. `Install the Archipelago Quality Of Life mod from JaredWeakStrike/AP_QOL using OpenKH Mod Manager`
4. `Install the mod from KH2FM-Mods-equations19/auto-save using OpenKH Mod Manager`
5. `AP Randomizer Seed`
1. [ArchipelagoKH2Client.exe](https://github.com/ArchipelagoMW/Archipelago/releases)
2. Install the Archipelago Companion mod from `JaredWeakStrike/APCompanion` using OpenKH Mod Manager
3. Install the mod from `KH2FM-Mods-equations19/auto-save` using OpenKH Mod Manager
4. Install the mod from `KH2FM-Mods-equations19/KH2-Lua-Library` using OpenKH Mod Manager
5. AP Randomizer Seed
- Optional Quality of Life Mods for Archipelago
1. Optionally Install the Archipelago Quality Of Life mod from `JaredWeakStrike/AP_QOL` using OpenKH Mod Manager
2. Optionally Install the Quality Of Life mod from `shananas/BearSkip` using OpenKH Mod Manager
<h3 style="text-transform:none";>Required: Archipelago Companion Mod</h3>
@@ -26,15 +29,21 @@ Load this mod just like the <b>GoA ROM</b> you did during the KH2 Rando setup. `
Have this mod second-highest priority below the .zip seed.<br>
This mod is based upon Num's Garden of Assemblege Mod and requires it to work. Without Num this could not be possible.
<h3 style="text-transform:none";>Required: Auto Save Mod</h3>
<h3 style="text-transform:none";>Required: Auto Save Mod and KH2 Lua Library</h3>
Load this mod just like the GoA ROM you did during the KH2 Rando setup. `KH2FM-Mods-equations19/auto-save` Location doesn't matter, required in case of crashes. See [Best Practices](en#best-practices) on how to load the auto save
Load these mods just like you loaded the GoA ROM mod during the KH2 Rando setup. `KH2FM-Mods-equations19/auto-save` and `KH2FM-Mods-equations19/KH2-Lua-Library` Location doesn't matter, required in case of crashes. See [Best Practices](en#best-practices) on how to load the auto save
<h3 style="text-transform:none";>Optional QoL Mods: AP QoL and Bear Skip</h3>
`JaredWeakStrike/AP_QOL` Makes the urns minigames much faster, makes Cavern of Remembrance orbs drop significantly more drive orbs for refilling drive/leveling master form, skips the animation when using the bulky vendor RC, skips carpet escape auto scroller in Agrabah 2, and prevents the wardrobe in the Beasts Castle wardrobe push minigame from waking up while being pushed.
`shananas/BearSkip` Skips all minigames in 100 Acre Woods except the Spooky Cave minigame since there are chests in Spooky Cave you can only get during the minigame. For Spooky Cave, Pooh is moved to the other side of the invisible wall that prevents you from using his RC to finish the minigame.
<h3 style="text-transform:none";>Installing A Seed</h3>
When you generate a game you will see a download link for a KH2 .zip seed on the room page. Download the seed then open OpenKH Mod Manager and click the green plus and `Select and install Mod Archive`.<br>
When you generate a game you will see a download link for a KH2 .zip seed on the room page. Download the seed then open OpenKH Mod Manager and click the green plus and "Select and install Mod Archive".<br>
Make sure the seed is on the top of the list (Highest Priority)<br>
After Installing the seed click `Mod Loader -> Build/Build and Run`. Every slot is a unique mod to install and will be needed be repatched for different slots/rooms.
After Installing the seed click "Mod Loader -> Build/Build and Run". Every slot is a unique mod to install and will be needed be repatched for different slots/rooms.
<h2 style="text-transform:none";>Optional Software:</h2>
@@ -48,18 +57,21 @@ After Installing the seed click `Mod Loader -> Build/Build and Run`. Every slot
<h2 style="text-transform:none";>Using the KH2 Client</h2>
Once you have started the game through OpenKH Mod Manager and are on the title screen run the [ArchipelagoKH2Client.exe](https://github.com/ArchipelagoMW/Archipelago/releases).<br>
Start the game through OpenKH Mod Manager. If starting a new run, enter the Garden of Assemblage from a new save. If returning to a run, load the save and enter the Garden of Assemblage. Then run the [ArchipelagoKH2Client.exe](https://github.com/ArchipelagoMW/Archipelago/releases).<br>
When you successfully connect to the server the client will automatically hook into the game to send/receive checks. <br>
If the client ever loses connection to the game, it will also disconnect from the server and you will need to reconnect.<br>
`Make sure the game is open whenever you try to connect the client to the server otherwise it will immediately disconnect you.`<br>
Make sure the game is open whenever you try to connect the client to the server otherwise it will immediately disconnect you.<br>
Most checks will be sent to you anywhere outside a load or cutscene.<br>
`If you obtain magic, you will need to pause your game to have it show up in your inventory, then enter a new room for it to become properly usable.`
If you obtain magic, you will need to pause your game to have it show up in your inventory, then enter a new room for it to become properly usable.
<h2 style="text-transform:none";>KH2 Client should look like this: </h2>
![image](https://i.imgur.com/qP6CmV8.png)
Enter `The room's port number` into the top box <b> where the x's are</b> and press "Connect". Follow the prompts there and you should be connected
Enter The room's port number into the top box <b> where the x's are</b> and press "Connect". Follow the prompts there and you should be connected
<h2 style="text-transform:none";>Common Pitfalls</h2>
@@ -102,7 +114,7 @@ This pack will handle logic, received items, checked locations and autotabbing f
- Why is my Client giving me a "Cannot Open Process: " error?
- Due to how the client reads kingdom hearts 2 memory some people's computer flags it as a virus. Run the client as admin.
- Why is my HP/MP continuously increasing without stopping?
- You do not have `JaredWeakStrike/APCompanion` set up correctly. Make sure it is above the `GoA ROM Mod` in the mod manager.
- You do not have `JaredWeakStrike/APCompanion` set up correctly. Make sure it is above the GoA ROM Edition Mod in the mod manager.
- Why is my HP/MP continuously increasing without stopping when I have the APCompanion Mod?
- You have a leftover GOA lua script in your `Documents\KINGDOM HEARTS HD 1.5+2.5 ReMIX\scripts\KH2`.
- Why am I missing worlds/portals in the GoA?
@@ -110,9 +122,9 @@ This pack will handle logic, received items, checked locations and autotabbing f
- Why did I not load into the correct visit?
- You need to trigger a cutscene or visit The World That Never Was for it to register that you have received the item.
- What versions of Kingdom Hearts 2 are supported?
- Currently the `only` supported versions are `Epic Games Version 1.0.0.9_WW` and `Steam Build Version 14716933`.
- Currently the only supported versions are Epic Games Version 1.0.0.10_WW and Steam Build Version 15194255.
- Why am I getting wallpapered while going into a world for the first time?
- Your `Lua Backend` was not configured correctly. Look over the step in the [KH2Rando.com](https://tommadness.github.io/KH2Randomizer/setup/Panacea-ModLoader/) guide.
- Your Lua Backend was not configured correctly. Look over the step in the [KH2Rando.com](https://tommadness.github.io/KH2Randomizer/setup/Panacea-ModLoader/) guide.
- Why am I not getting magic?
- If you obtain magic, you will need to pause your game to have it show up in your inventory, then enter a new room for it to become properly usable.
- Why did I crash after picking my dream weapon?
@@ -124,7 +136,7 @@ This pack will handle logic, received items, checked locations and autotabbing f
- You will need to get the `JaredWeakStrike/APCompanion` (you can find how to get this if you scroll up)
- Why am I not sending or receiving items?
- Make sure you are connected to the KH2 client and the correct room (for more information scroll up)
- Why should I install the auto save mod at `KH2FM-Mods-equations19/auto-save`?
- Because Kingdom Hearts 2 is prone to crashes and will keep you from losing your progress.
- Why should I install the auto save mod at `KH2FM-Mods-equations19/auto-save` and `KH2FM-Mods-equations19/KH2-Lua-Library`?
- Because Kingdom Hearts 2 is prone to crashes and will keep you from losing your progress. Both mods are needed for auto save to work.
- How do I load an auto save?
- To load an auto-save, hold down the Select or your equivalent on your prefered controller while choosing a file. Make sure to hold the button down the whole time.

View File

@@ -381,7 +381,7 @@ class MessengerWorld(World):
return
# the messenger client calls into AP with specific args, so check the out path matches what the client sends
out_path = output_path(multiworld.get_out_file_name_base(1) + ".aptm")
if "The Messenger\\Archipelago\\output" not in out_path:
if "Messenger\\Archipelago\\output" not in out_path:
return
import orjson
data = {

View File

@@ -749,8 +749,8 @@ location_data = [
LocationData("Cinnabar Gym", "Super Nerd 2", None, rom_addresses["Trainersanity_EVENT_BEAT_CINNABAR_GYM_TRAINER_2_ITEM"], EventFlag(372), inclusion=trainersanity),
LocationData("Cinnabar Gym", "Burglar 2", None, rom_addresses["Trainersanity_EVENT_BEAT_CINNABAR_GYM_TRAINER_3_ITEM"], EventFlag(371), inclusion=trainersanity),
LocationData("Cinnabar Gym", "Super Nerd 3", None, rom_addresses["Trainersanity_EVENT_BEAT_CINNABAR_GYM_TRAINER_4_ITEM"], EventFlag(370), inclusion=trainersanity),
LocationData("Cinnabar Gym", "Super Nerd 4", None, rom_addresses["Trainersanity_EVENT_BEAT_CINNABAR_GYM_TRAINER_5_ITEM"], EventFlag(369), inclusion=trainersanity),
LocationData("Cinnabar Gym", "Super Nerd 5", None, rom_addresses["Trainersanity_EVENT_BEAT_CINNABAR_GYM_TRAINER_6_ITEM"], EventFlag(368), inclusion=trainersanity),
LocationData("Cinnabar Gym", "Burglar 3", None, rom_addresses["Trainersanity_EVENT_BEAT_CINNABAR_GYM_TRAINER_5_ITEM"], EventFlag(369), inclusion=trainersanity),
LocationData("Cinnabar Gym", "Super Nerd 4", None, rom_addresses["Trainersanity_EVENT_BEAT_CINNABAR_GYM_TRAINER_6_ITEM"], EventFlag(368), inclusion=trainersanity),
LocationData("Celadon Prize Corner", "Item Prize 1", "TM23 Dragon Rage", rom_addresses["Prize_Item_A"], EventFlag(0x69a), inclusion=prizesanity),
LocationData("Celadon Prize Corner", "Item Prize 2", "TM15 Hyper Beam", rom_addresses["Prize_Item_B"], EventFlag(0x69B), inclusion=prizesanity),

View File

@@ -1718,7 +1718,7 @@ def create_regions(world):
connect(multiworld, player, "Vermilion City", "Vermilion City-Dock", lambda state: state.has("S.S. Ticket", player))
connect(multiworld, player, "Vermilion City", "Route 11")
connect(multiworld, player, "Route 12-N", "Route 12-S", lambda state: logic.can_surf(state, world, player))
connect(multiworld, player, "Route 12-W", "Route 11-E", lambda state: state.has("Poke Flute", player))
connect(multiworld, player, "Route 12-W", "Route 11-E")
connect(multiworld, player, "Route 12-W", "Route 12-N", lambda state: state.has("Poke Flute", player))
connect(multiworld, player, "Route 12-W", "Route 12-S", lambda state: state.has("Poke Flute", player))
connect(multiworld, player, "Route 12-S", "Route 12-Grass", lambda state: logic.can_cut(state, world, player), one_way=True)

View File

@@ -13,7 +13,7 @@ class ItemType(enum.Enum):
POT = "pot"
POT_COMPLETE = "pot-complete"
POT_DUPLICATE = "pot-duplicate"
POT_COMPELTE_DUPLICATE = "pot-complete-duplicate"
POT_COMPLETE_DUPLICATE = "pot-complete-duplicate"
KEY = "key"
KEY_OPTIONAL = "key-optional"
ABILITY = "ability"
@@ -117,16 +117,16 @@ item_table = {
"Lightning Pot Top DUPE": ItemData(None, ItemType.POT_DUPLICATE),
"Sand Pot Top DUPE": ItemData(None, ItemType.POT_DUPLICATE),
"Metal Pot Top DUPE": ItemData(None, ItemType.POT_DUPLICATE),
"Water Pot Complete DUPE": ItemData(None, ItemType.POT_COMPELTE_DUPLICATE),
"Wax Pot Complete DUPE": ItemData(None, ItemType.POT_COMPELTE_DUPLICATE),
"Ash Pot Complete DUPE": ItemData(None, ItemType.POT_COMPELTE_DUPLICATE),
"Oil Pot Complete DUPE": ItemData(None, ItemType.POT_COMPELTE_DUPLICATE),
"Cloth Pot Complete DUPE": ItemData(None, ItemType.POT_COMPELTE_DUPLICATE),
"Wood Pot Complete DUPE": ItemData(None, ItemType.POT_COMPELTE_DUPLICATE),
"Crystal Pot Complete DUPE": ItemData(None, ItemType.POT_COMPELTE_DUPLICATE),
"Lightning Pot Complete DUPE": ItemData(None, ItemType.POT_COMPELTE_DUPLICATE),
"Sand Pot Complete DUPE": ItemData(None, ItemType.POT_COMPELTE_DUPLICATE),
"Metal Pot Complete DUPE": ItemData(None, ItemType.POT_COMPELTE_DUPLICATE),
"Water Pot Complete DUPE": ItemData(None, ItemType.POT_COMPLETE_DUPLICATE),
"Wax Pot Complete DUPE": ItemData(None, ItemType.POT_COMPLETE_DUPLICATE),
"Ash Pot Complete DUPE": ItemData(None, ItemType.POT_COMPLETE_DUPLICATE),
"Oil Pot Complete DUPE": ItemData(None, ItemType.POT_COMPLETE_DUPLICATE),
"Cloth Pot Complete DUPE": ItemData(None, ItemType.POT_COMPLETE_DUPLICATE),
"Wood Pot Complete DUPE": ItemData(None, ItemType.POT_COMPLETE_DUPLICATE),
"Crystal Pot Complete DUPE": ItemData(None, ItemType.POT_COMPLETE_DUPLICATE),
"Lightning Pot Complete DUPE": ItemData(None, ItemType.POT_COMPLETE_DUPLICATE),
"Sand Pot Complete DUPE": ItemData(None, ItemType.POT_COMPLETE_DUPLICATE),
"Metal Pot Complete DUPE": ItemData(None, ItemType.POT_COMPLETE_DUPLICATE),
# Filler
"Empty": ItemData(None, ItemType.FILLER, ItemClassification.filler),

View File

@@ -1,7 +1,8 @@
from dataclasses import dataclass
from Options import (
Choice, DefaultOnToggle, ItemDict, ItemSet, LocationSet, OptionGroup, PerGameCommonOptions, Range, Toggle,
Choice, DefaultOnToggle, ExcludeLocations, LocalItems, NonLocalItems, OptionGroup, PerGameCommonOptions,
PriorityLocations, Range, StartHints, StartInventory, StartLocationHints, Toggle,
)
from . import ItemType, item_table
from .Constants import location_info
@@ -109,6 +110,13 @@ class FullPots(Choice):
option_mixed = 2
class IxupiCapturesPriority(DefaultOnToggle):
"""
Ixupi captures are set to priority locations. This forces a progression item into these locations if possible.
"""
display_name = "Ixupi Captures are Priority"
class PuzzleCollectBehavior(Choice):
"""
Defines what happens to puzzles on collect.
@@ -129,53 +137,38 @@ valid_item_keys = [name for name, data in item_table.items() if data.type != Ite
valid_location_keys = [name for name in location_info["all_locations"] if name != "Mystery Solved"]
class LocalItems(ItemSet):
"""Forces these items to be in their native world."""
display_name = "Local Items"
rich_text_doc = True
class ShiversLocalItems(LocalItems):
__doc__ = LocalItems.__doc__
valid_keys = valid_item_keys
class NonLocalItems(ItemSet):
"""Forces these items to be outside their native world."""
display_name = "Non-local Items"
rich_text_doc = True
class ShiversNonLocalItems(NonLocalItems):
__doc__ = NonLocalItems.__doc__
valid_keys = valid_item_keys
class StartInventory(ItemDict):
"""Start with these items."""
verify_item_name = True
display_name = "Start Inventory"
rich_text_doc = True
class ShiversStartInventory(StartInventory):
__doc__ = StartInventory.__doc__
valid_keys = valid_item_keys
class StartHints(ItemSet):
"""Start with these item's locations prefilled into the ``!hint`` command."""
display_name = "Start Hints"
rich_text_doc = True
class ShiversStartHints(StartHints):
__doc__ = StartHints.__doc__
valid_keys = valid_item_keys
class StartLocationHints(LocationSet):
"""Start with these locations and their item prefilled into the ``!hint`` command."""
display_name = "Start Location Hints"
rich_text_doc = True
class ShiversStartLocationHints(StartLocationHints):
__doc__ = StartLocationHints.__doc__
valid_keys = valid_location_keys
class ExcludeLocations(LocationSet):
"""Prevent these locations from having an important item."""
display_name = "Excluded Locations"
rich_text_doc = True
class ShiversExcludeLocations(ExcludeLocations):
__doc__ = ExcludeLocations.__doc__
valid_keys = valid_location_keys
class PriorityLocations(LocationSet):
"""Prevent these locations from having an unimportant item."""
display_name = "Priority Locations"
rich_text_doc = True
class ShiversPriorityLocations(PriorityLocations):
__doc__ = PriorityLocations.__doc__
valid_keys = valid_location_keys
@@ -191,24 +184,27 @@ class ShiversOptions(PerGameCommonOptions):
early_lightning: EarlyLightning
location_pot_pieces: LocationPotPieces
full_pots: FullPots
ixupi_captures_priority: IxupiCapturesPriority
puzzle_collect_behavior: PuzzleCollectBehavior
local_items: LocalItems
non_local_items: NonLocalItems
start_inventory: StartInventory
start_hints: StartHints
start_location_hints: StartLocationHints
exclude_locations: ExcludeLocations
priority_locations: PriorityLocations
local_items: ShiversLocalItems
non_local_items: ShiversNonLocalItems
start_inventory: ShiversStartInventory
start_hints: ShiversStartHints
start_location_hints: ShiversStartLocationHints
exclude_locations: ShiversExcludeLocations
priority_locations: ShiversPriorityLocations
shivers_option_groups = [
OptionGroup("Item & Location Options", [
LocalItems,
NonLocalItems,
StartInventory,
StartHints,
StartLocationHints,
ExcludeLocations,
PriorityLocations
], True),
OptionGroup(
"Item & Location Options", [
ShiversLocalItems,
ShiversNonLocalItems,
ShiversStartInventory,
ShiversStartHints,
ShiversStartLocationHints,
ShiversExcludeLocations,
ShiversPriorityLocations
], True,
),
]

View File

@@ -16,7 +16,7 @@ class ShiversWeb(WebWorld):
"English",
"setup_en.md",
"setup/en",
["GodlFire", "Mathx2"]
["GodlFire", "Cynbel_Terreus"]
)]
option_groups = shivers_option_groups
@@ -41,6 +41,20 @@ class ShiversWorld(World):
def generate_early(self):
self.pot_completed_list = []
# Pot piece shuffle location:
if self.options.location_pot_pieces == "own_world":
self.options.local_items.value |= {name for name, data in item_table.items() if
data.type in [ItemType.POT, ItemType.POT_COMPLETE]}
elif self.options.location_pot_pieces == "different_world":
self.options.non_local_items.value |= {name for name, data in item_table.items() if
data.type in [ItemType.POT, ItemType.POT_COMPLETE]}
# Ixupi captures priority locations:
if self.options.ixupi_captures_priority:
self.options.priority_locations.value |= (
{name for name in self.location_names if name.startswith('Ixupi Captured')}
)
def create_item(self, name: str) -> Item:
data = item_table[name]
return ShiversItem(name, data.classification, data.code, self.player)
@@ -194,14 +208,6 @@ class ShiversWorld(World):
["Heal", "Easier Lyre"], weights=[95, 5], k=filler_needed
))
# Pot piece shuffle location:
if self.options.location_pot_pieces == "own_world":
self.options.local_items.value |= {name for name, data in item_table.items() if
data.type in [ItemType.POT, ItemType.POT_COMPLETE]}
elif self.options.location_pot_pieces == "different_world":
self.options.non_local_items.value |= {name for name, data in item_table.items() if
data.type in [ItemType.POT, ItemType.POT_COMPLETE]}
self.multiworld.itempool += item_pool
def pre_fill(self) -> None:
@@ -220,13 +226,13 @@ class ShiversWorld(World):
data.type == ItemType.POT_DUPLICATE]
elif self.options.full_pots == "complete":
storage_items += [self.create_item(name) for name, data in item_table.items() if
data.type == ItemType.POT_COMPELTE_DUPLICATE]
data.type == ItemType.POT_COMPLETE_DUPLICATE]
storage_items += [self.create_item("Empty") for _ in range(10)]
else:
pieces = [self.create_item(name) for name, data in item_table.items() if
data.type == ItemType.POT_DUPLICATE]
complete = [self.create_item(name) for name, data in item_table.items() if
data.type == ItemType.POT_COMPELTE_DUPLICATE]
data.type == ItemType.POT_COMPLETE_DUPLICATE]
for i in range(10):
# Pieces
if self.pot_completed_list[i] == 0:

View File

@@ -7,6 +7,11 @@
- [ScummVM](https://www.scummvm.org/downloads/) version 2.7.0 or later
- [Shivers Randomizer](https://github.com/GodlFire/Shivers-Randomizer-CSharp/releases/latest) Latest release version
## Optional Software
- [PopTracker](https://github.com/black-sliver/PopTracker/releases/)
- [Jax's Shivers PopTracker pack](https://github.com/blazik-barth/Shivers-Tracker/releases/)
## Setup ScummVM for Shivers
### GOG version of Shivers
@@ -57,4 +62,4 @@ validator page: [YAML Validation page](/mysterycheck)
- Every puzzle
- Every puzzle hint/solution
- Every document that is considered a Flashback
- Optionally information plaques.
- Optionally information plaques

View File

@@ -4,18 +4,59 @@ import os
import json
import Utils
from Utils import read_snes_rom
from worlds.Files import APDeltaPatch
from worlds.Files import APPatchExtension, APProcedurePatch, APTokenMixin, APTokenTypes
from .variaRandomizer.utils.utils import openFile
SMJUHASH = '21f3e98df4780ee1c667b84e57d88675'
SM_ROM_MAX_PLAYERID = 65535
SM_ROM_PLAYERDATA_COUNT = 202
class SMDeltaPatch(APDeltaPatch):
class SMPatchExtensions(APPatchExtension):
game = "Super Metroid"
@staticmethod
def write_crc(caller: APProcedurePatch, rom: bytes) -> bytes:
def checksum_mirror_sum(start, length, mask = 0x800000):
while not(length & mask) and mask:
mask >>= 1
part1 = sum(start[:mask]) & 0xFFFF
part2 = 0
next_length = length - mask
if next_length:
part2 = checksum_mirror_sum(start[mask:], next_length, mask >> 1)
while (next_length < mask):
next_length += next_length
part2 += part2
return (part1 + part2) & 0xFFFF
def write_bytes(buffer, startaddress: int, values):
buffer[startaddress:startaddress + len(values)] = values
buffer = bytearray(rom)
crc = checksum_mirror_sum(buffer, len(buffer))
inv = crc ^ 0xFFFF
write_bytes(buffer, 0x7FDC, [inv & 0xFF, (inv >> 8) & 0xFF, crc & 0xFF, (crc >> 8) & 0xFF])
return bytes(buffer)
class SMProcedurePatch(APProcedurePatch, APTokenMixin):
hash = SMJUHASH
game = "Super Metroid"
patch_file_ending = ".apsm"
procedure = [
("apply_tokens", ["token_data.bin"]),
("write_crc", [])
]
def write_tokens(self, patches):
for addr, data in patches.items():
self.write_token(APTokenTypes.WRITE, addr, bytes(data))
self.write_file("token_data.bin", self.get_token_binary())
@classmethod
def get_source_data(cls) -> bytes:
return get_base_rom_bytes()

View File

@@ -17,7 +17,7 @@ logger = logging.getLogger("Super Metroid")
from .Options import SMOptions
from .Client import SMSNIClient
from .Rom import get_base_rom_path, SM_ROM_MAX_PLAYERID, SM_ROM_PLAYERDATA_COUNT, SMDeltaPatch, get_sm_symbols
from .Rom import SM_ROM_MAX_PLAYERID, SM_ROM_PLAYERDATA_COUNT, SMProcedurePatch, get_sm_symbols
import Utils
from .variaRandomizer.logic.smboolmanager import SMBoolManager
@@ -40,7 +40,7 @@ class SMSettings(settings.Group):
"""File name of the v1.0 J rom"""
description = "Super Metroid (JU) ROM"
copy_to = "Super Metroid (JU).sfc"
md5s = [SMDeltaPatch.hash]
md5s = [SMProcedurePatch.hash]
rom_file: RomFile = RomFile(RomFile.copy_to)
@@ -120,12 +120,6 @@ class SMWorld(World):
self.locations = {}
super().__init__(world, player)
@classmethod
def stage_assert_generate(cls, multiworld: MultiWorld):
rom_file = get_base_rom_path()
if not os.path.exists(rom_file):
raise FileNotFoundError(rom_file)
def generate_early(self):
Logic.factory('vanilla')
@@ -802,23 +796,19 @@ class SMWorld(World):
romPatcher.end()
def generate_output(self, output_directory: str):
self.variaRando.args.rom = get_base_rom_path()
outfilebase = self.multiworld.get_out_file_name_base(self.player)
outputFilename = os.path.join(output_directory, f"{outfilebase}.sfc")
try:
self.variaRando.PatchRom(outputFilename, self.APPrePatchRom, self.APPostPatchRom)
self.write_crc(outputFilename)
patcher = self.variaRando.PatchRom(self.APPrePatchRom, self.APPostPatchRom)
self.rom_name = self.romName
patch = SMProcedurePatch(player=self.player, player_name=self.multiworld.player_name[self.player])
patch.write_tokens(patcher.romFile.getPatchDict())
rom_path = os.path.join(output_directory, f"{self.multiworld.get_out_file_name_base(self.player)}"
f"{patch.patch_file_ending}")
patch.write(rom_path)
except:
raise
else:
patch = SMDeltaPatch(os.path.splitext(outputFilename)[0] + SMDeltaPatch.patch_file_ending, player=self.player,
player_name=self.multiworld.player_name[self.player], patched_path=outputFilename)
patch.write()
finally:
if os.path.exists(outputFilename):
os.unlink(outputFilename)
self.rom_name_available_event.set() # make sure threading continues and errors are collected
def checksum_mirror_sum(self, start, length, mask = 0x800000):

View File

@@ -680,7 +680,7 @@ class VariaRandomizer:
#dumpErrorMsg(args.output, self.randoExec.errorMsg)
raise Exception("Can't generate " + self.fileName + " with the given parameters: {}".format(self.randoExec.errorMsg))
def PatchRom(self, outputFilename, customPrePatchApply = None, customPostPatchApply = None):
def PatchRom(self, customPrePatchApply = None, customPostPatchApply = None) -> RomPatcher:
args = self.args
optErrMsgs = self.optErrMsgs
@@ -758,9 +758,9 @@ class VariaRandomizer:
# args.output is not None: generate local json named args.output
if args.rom is not None:
# patch local rom
romFileName = args.rom
shutil.copyfile(romFileName, outputFilename)
romPatcher = RomPatcher(settings=patcherSettings, romFileName=outputFilename, magic=args.raceMagic, player=self.player)
# romFileName = args.rom
# shutil.copyfile(romFileName, outputFilename)
romPatcher = RomPatcher(settings=patcherSettings, magic=args.raceMagic, player=self.player)
else:
romPatcher = RomPatcher(settings=patcherSettings, magic=args.raceMagic)
@@ -779,24 +779,12 @@ class VariaRandomizer:
#msg = randoExec.errorMsg
msg = ''
if args.rom is None: # web mode
data = romPatcher.romFile.data
self.fileName = '{}.sfc'.format(self.fileName)
data["fileName"] = self.fileName
# error msg in json to be displayed by the web site
data["errorMsg"] = msg
# replaced parameters to update stats in database
if len(self.forcedArgs) > 0:
data["forcedArgs"] = self.forcedArgs
with open(outputFilename, 'w') as jsonFile:
json.dump(data, jsonFile)
else: # CLI mode
if msg != "":
print(msg)
return romPatcher
except Exception as e:
import traceback
traceback.print_exc(file=sys.stdout)
raise Exception("Error patching {}: ({}: {})".format(outputFilename, type(e).__name__, e))
raise Exception("Error patching: ({}: {})".format(type(e).__name__, e))
#dumpErrorMsg(args.output, msg)
# if stuck == True:

View File

@@ -21,10 +21,23 @@ class IPS_Patch(object):
def toDict(self):
ret = {}
for record in self.records:
if 'rle_count' in record:
ret[record['address']] = [int.from_bytes(record['data'],'little')]*record['rle_count']
if record['address'] in ret.keys():
if 'rle_count' in record:
if len(ret[record['address']]) > record['rle_count']:
ret[record['address']][:record['rle_count']] = [int.from_bytes(record['data'],'little')]*record['rle_count']
else:
ret[record['address']] = [int.from_bytes(record['data'],'little')]*record['rle_count']
else:
size = len(record['data'])
if len(ret[record['address']]) > size:
ret[record['address']][:size] = [int(b) for b in record['data']]
else:
ret[record['address']] = [int(b) for b in record['data']]
else:
ret[record['address']] = [int(b) for b in record['data']]
if 'rle_count' in record:
ret[record['address']] = [int.from_bytes(record['data'],'little')]*record['rle_count']
else:
ret[record['address']] = [int(b) for b in record['data']]
return ret
@staticmethod

View File

@@ -86,7 +86,67 @@ class ROM(object):
self.seek(self.maxAddress + BANK_SIZE - off - 1)
self.writeByte(0xff)
assert (self.maxAddress % BANK_SIZE) == 0
class FakeROM(ROM):
# to have the same code for real ROM and the webservice
def __init__(self, data={}):
super(FakeROM, self).__init__()
self.data = data
self.ipsPatches = []
def write(self, bytes):
for byte in bytes:
self.data[self.address] = byte
self.inc()
def read(self, byteCount):
bytes = []
for i in range(byteCount):
bytes.append(self.data[self.address])
self.inc()
return bytes
def ipsPatch(self, ipsPatches):
self.ipsPatches += ipsPatches
# generate ips from self data
def ips(self):
groupedData = {}
startAddress = -1
prevAddress = -1
curData = []
for address in sorted(self.data):
if address == prevAddress + 1:
curData.append(self.data[address])
prevAddress = address
else:
if len(curData) > 0:
groupedData[startAddress] = curData
startAddress = address
prevAddress = address
curData = [self.data[startAddress]]
if startAddress != -1:
groupedData[startAddress] = curData
return IPS_Patch(groupedData)
# generate final IPS for web patching with first the IPS patches, then written data
def close(self):
self.mergedIPS = IPS_Patch()
for ips in self.ipsPatches:
self.mergedIPS.append(ips)
self.mergedIPS.append(self.ips())
#patchData = mergedIPS.encode()
#self.data = {}
#self.data["ips"] = base64.b64encode(patchData).decode()
#if mergedIPS.truncate_length is not None:
# self.data["truncate_length"] = mergedIPS.truncate_length
#self.data["max_size"] = mergedIPS.max_size
def getPatchDict(self):
return self.mergedIPS.toDict()
class RealROM(ROM):
def __init__(self, name):
super(RealROM, self).__init__()

View File

@@ -7,7 +7,7 @@ from ..utils.doorsmanager import DoorsManager, IndicatorFlag
from ..utils.objectives import Objectives
from ..graph.graph_utils import GraphUtils, getAccessPoint, locIdsByAreaAddresses, graphAreas
from ..logic.logic import Logic
from ..rom.rom import RealROM, snes_to_pc, pc_to_snes
from ..rom.rom import FakeROM, snes_to_pc, pc_to_snes
from ..rom.addresses import Addresses
from ..rom.rom_patches import RomPatches
from ..patches.patchaccess import PatchAccess
@@ -52,10 +52,10 @@ class RomPatcher:
def __init__(self, settings=None, romFileName=None, magic=None, player=0):
self.log = log.get('RomPatcher')
self.settings = settings
self.romFileName = romFileName
#self.romFileName = romFileName
self.patchAccess = PatchAccess()
self.race = None
self.romFile = RealROM(romFileName)
self.romFile = FakeROM()
#if magic is not None:
# from rom.race_mode import RaceModePatcher
# self.race = RaceModePatcher(self, magic)
@@ -312,7 +312,7 @@ class RomPatcher:
self.applyStartAP(self.settings["startLocation"], plms, doors)
self.applyPLMs(plms)
except Exception as e:
raise Exception("Error patching {}. ({})".format(self.romFileName, e))
raise Exception("Error patching. ({})".format(e))
def applyIPSPatch(self, patchName, patchDict=None, ipsDir=None):
if patchDict is None:
@@ -493,6 +493,7 @@ class RomPatcher:
def commitIPS(self):
self.romFile.ipsPatch(self.ipsPatches)
self.ipsPatches = []
def writeSeed(self, seed):
random.seed(seed)

View File

@@ -772,6 +772,7 @@ if 'unittest' in sys.modules.keys() or 'pytest' in sys.modules.keys():
class Mods(OptionSet):
"""List of mods that will be included in the shuffling."""
visibility = Visibility.all & ~Visibility.simple_ui
internal_name = "mods"
display_name = "Mods"
valid_keys = {ModNames.deepwoods, ModNames.tractor, ModNames.big_backpack,

View File

@@ -1,6 +1,5 @@
from __future__ import annotations
import logging
import itertools
from typing import List, Dict, Any, cast
@@ -10,13 +9,11 @@ from . import items
from . import locations
from . import creatures
from . import options
from .items import item_table, group_items, items_by_type, ItemType
from .items import item_table, group_items
from .rules import set_rules
logger = logging.getLogger("Subnautica")
class SubnaticaWeb(WebWorld):
class SubnauticaWeb(WebWorld):
tutorials = [Tutorial(
"Multiworld Setup Guide",
"A guide to setting up the Subnautica randomizer connected to an Archipelago Multiworld",
@@ -38,7 +35,7 @@ class SubnauticaWorld(World):
You must find a cure for yourself, build an escape rocket, and leave the planet.
"""
game = "Subnautica"
web = SubnaticaWeb()
web = SubnauticaWeb()
item_name_to_id = {data.name: item_id for item_id, data in items.item_table.items()}
location_name_to_id = all_locations

View File

@@ -90,6 +90,10 @@ class TunicWorld(World):
item_link_locations: Dict[int, Dict[str, List[Tuple[int, str]]]] = {}
player_item_link_locations: Dict[str, List[Location]]
using_ut: bool # so we can check if we're using UT only once
passthrough: Dict[str, Any]
ut_can_gen_without_yaml = True # class var that tells it to ignore the player yaml
def generate_early(self) -> None:
if self.options.logic_rules >= LogicRules.option_no_major_glitches:
self.options.laurels_zips.value = LaurelsZips.option_true
@@ -113,23 +117,28 @@ class TunicWorld(World):
# Universal tracker stuff, shouldn't do anything in standard gen
if hasattr(self.multiworld, "re_gen_passthrough"):
if "TUNIC" in self.multiworld.re_gen_passthrough:
passthrough = self.multiworld.re_gen_passthrough["TUNIC"]
self.options.start_with_sword.value = passthrough["start_with_sword"]
self.options.keys_behind_bosses.value = passthrough["keys_behind_bosses"]
self.options.sword_progression.value = passthrough["sword_progression"]
self.options.ability_shuffling.value = passthrough["ability_shuffling"]
self.options.laurels_zips.value = passthrough["laurels_zips"]
self.options.ice_grappling.value = passthrough["ice_grappling"]
self.options.ladder_storage.value = passthrough["ladder_storage"]
self.options.ladder_storage_without_items = passthrough["ladder_storage_without_items"]
self.options.lanternless.value = passthrough["lanternless"]
self.options.maskless.value = passthrough["maskless"]
self.options.hexagon_quest.value = passthrough["hexagon_quest"]
self.options.entrance_rando.value = passthrough["entrance_rando"]
self.options.shuffle_ladders.value = passthrough["shuffle_ladders"]
self.using_ut = True
self.passthrough = self.multiworld.re_gen_passthrough["TUNIC"]
self.options.start_with_sword.value = self.passthrough["start_with_sword"]
self.options.keys_behind_bosses.value = self.passthrough["keys_behind_bosses"]
self.options.sword_progression.value = self.passthrough["sword_progression"]
self.options.ability_shuffling.value = self.passthrough["ability_shuffling"]
self.options.laurels_zips.value = self.passthrough["laurels_zips"]
self.options.ice_grappling.value = self.passthrough["ice_grappling"]
self.options.ladder_storage.value = self.passthrough["ladder_storage"]
self.options.ladder_storage_without_items = self.passthrough["ladder_storage_without_items"]
self.options.lanternless.value = self.passthrough["lanternless"]
self.options.maskless.value = self.passthrough["maskless"]
self.options.hexagon_quest.value = self.passthrough["hexagon_quest"]
self.options.entrance_rando.value = self.passthrough["entrance_rando"]
self.options.shuffle_ladders.value = self.passthrough["shuffle_ladders"]
self.options.fixed_shop.value = self.options.fixed_shop.option_false
self.options.laurels_location.value = self.options.laurels_location.option_anywhere
self.options.combat_logic.value = passthrough["combat_logic"]
self.options.combat_logic.value = self.passthrough["combat_logic"]
else:
self.using_ut = False
else:
self.using_ut = False
@classmethod
def stage_generate_early(cls, multiworld: MultiWorld) -> None:
@@ -284,12 +293,14 @@ class TunicWorld(World):
remove_filler(items_to_create[gold_hexagon])
for hero_relic in item_name_groups["Hero Relics"]:
# Sort for deterministic order
for hero_relic in sorted(item_name_groups["Hero Relics"]):
tunic_items.append(self.create_item(hero_relic, ItemClassification.useful))
items_to_create[hero_relic] = 0
if not self.options.ability_shuffling:
for page in item_name_groups["Abilities"]:
# Sort for deterministic order
for page in sorted(item_name_groups["Abilities"]):
if items_to_create[page] > 0:
tunic_items.append(self.create_item(page, ItemClassification.useful))
items_to_create[page] = 0
@@ -329,12 +340,10 @@ class TunicWorld(World):
self.ability_unlocks = randomize_ability_unlocks(self.random, self.options)
# stuff for universal tracker support, can be ignored for standard gen
if hasattr(self.multiworld, "re_gen_passthrough"):
if "TUNIC" in self.multiworld.re_gen_passthrough:
passthrough = self.multiworld.re_gen_passthrough["TUNIC"]
self.ability_unlocks["Pages 24-25 (Prayer)"] = passthrough["Hexagon Quest Prayer"]
self.ability_unlocks["Pages 42-43 (Holy Cross)"] = passthrough["Hexagon Quest Holy Cross"]
self.ability_unlocks["Pages 52-53 (Icebolt)"] = passthrough["Hexagon Quest Icebolt"]
if self.using_ut:
self.ability_unlocks["Pages 24-25 (Prayer)"] = self.passthrough["Hexagon Quest Prayer"]
self.ability_unlocks["Pages 42-43 (Holy Cross)"] = self.passthrough["Hexagon Quest Holy Cross"]
self.ability_unlocks["Pages 52-53 (Icebolt)"] = self.passthrough["Hexagon Quest Icebolt"]
# Ladders and Combat Logic uses ER rules with vanilla connections for easier maintenance
if self.options.entrance_rando or self.options.shuffle_ladders or self.options.combat_logic:

View File

@@ -1675,7 +1675,7 @@ def set_er_location_rules(world: "TunicWorld") -> None:
# Beneath the Vault
set_rule(world.get_location("Beneath the Fortress - Bridge"),
lambda state: has_melee(state, player) or state.has_any({laurels, fire_wand}, player))
lambda state: has_melee(state, player) or state.has_any((laurels, fire_wand, ice_dagger, gun), player))
# Quarry
set_rule(world.get_location("Quarry - [Central] Above Ladder Dash Chest"),

View File

@@ -177,7 +177,7 @@ def pair_portals(world: "TunicWorld", regions: Dict[str, Region]) -> Dict[Portal
logic_tricks: Tuple[bool, int, int] = (laurels_zips, ice_grappling, ladder_storage)
# marking that you don't immediately have laurels
if laurels_location == "10_fairies" and not hasattr(world.multiworld, "re_gen_passthrough"):
if laurels_location == "10_fairies" and not world.using_ut:
has_laurels = False
shop_count = 6
@@ -191,9 +191,8 @@ def pair_portals(world: "TunicWorld", regions: Dict[str, Region]) -> Dict[Portal
break
# If using Universal Tracker, restore portal_map. Could be cleaner, but it does not matter for UT even a little bit
if hasattr(world.multiworld, "re_gen_passthrough"):
if "TUNIC" in world.multiworld.re_gen_passthrough:
portal_map = portal_mapping.copy()
if world.using_ut:
portal_map = portal_mapping.copy()
# create separate lists for dead ends and non-dead ends
for portal in portal_map:
@@ -232,25 +231,24 @@ def pair_portals(world: "TunicWorld", regions: Dict[str, Region]) -> Dict[Portal
plando_connections = world.seed_groups[world.options.entrance_rando.value]["plando"]
# universal tracker support stuff, don't need to care about region dependency
if hasattr(world.multiworld, "re_gen_passthrough"):
if "TUNIC" in world.multiworld.re_gen_passthrough:
plando_connections.clear()
# universal tracker stuff, won't do anything in normal gen
for portal1, portal2 in world.multiworld.re_gen_passthrough["TUNIC"]["Entrance Rando"].items():
portal_name1 = ""
portal_name2 = ""
if world.using_ut:
plando_connections.clear()
# universal tracker stuff, won't do anything in normal gen
for portal1, portal2 in world.passthrough["Entrance Rando"].items():
portal_name1 = ""
portal_name2 = ""
for portal in portal_mapping:
if portal.scene_destination() == portal1:
portal_name1 = portal.name
# connected_regions.update(add_dependent_regions(portal.region, logic_rules))
if portal.scene_destination() == portal2:
portal_name2 = portal.name
# connected_regions.update(add_dependent_regions(portal.region, logic_rules))
# shops have special handling
if not portal_name2 and portal2 == "Shop, Previous Region_":
portal_name2 = "Shop Portal"
plando_connections.append(PlandoConnection(portal_name1, portal_name2, "both"))
for portal in portal_mapping:
if portal.scene_destination() == portal1:
portal_name1 = portal.name
# connected_regions.update(add_dependent_regions(portal.region, logic_rules))
if portal.scene_destination() == portal2:
portal_name2 = portal.name
# connected_regions.update(add_dependent_regions(portal.region, logic_rules))
# shops have special handling
if not portal_name2 and portal2 == "Shop, Previous Region_":
portal_name2 = "Shop Portal"
plando_connections.append(PlandoConnection(portal_name1, portal_name2, "both"))
non_dead_end_regions = set()
for region_name, region_info in world.er_regions.items():
@@ -362,7 +360,7 @@ def pair_portals(world: "TunicWorld", regions: Dict[str, Region]) -> Dict[Portal
# if we have plando connections, our connected regions may change somewhat
connected_regions = update_reachable_regions(connected_regions, traversal_reqs, has_laurels, logic_tricks)
if fixed_shop and not hasattr(world.multiworld, "re_gen_passthrough"):
if fixed_shop and not world.using_ut:
portal1 = None
for portal in two_plus:
if portal.scene_destination() == "Overworld Redux, Windmill_":
@@ -392,7 +390,7 @@ def pair_portals(world: "TunicWorld", regions: Dict[str, Region]) -> Dict[Portal
fail_count = 0
while len(connected_regions) < len(non_dead_end_regions):
# if this is universal tracker, just break immediately and move on
if hasattr(world.multiworld, "re_gen_passthrough"):
if world.using_ut:
break
# if the connected regions length stays unchanged for too long, it's stuck in a loop
# should, hopefully, only ever occur if someone plandos connections poorly
@@ -445,9 +443,8 @@ def pair_portals(world: "TunicWorld", regions: Dict[str, Region]) -> Dict[Portal
random_object.shuffle(two_plus)
# for universal tracker, we want to skip shop gen
if hasattr(world.multiworld, "re_gen_passthrough"):
if "TUNIC" in world.multiworld.re_gen_passthrough:
shop_count = 0
if world.using_ut:
shop_count = 0
for i in range(shop_count):
portal1 = two_plus.pop()
@@ -462,7 +459,7 @@ def pair_portals(world: "TunicWorld", regions: Dict[str, Region]) -> Dict[Portal
# connect dead ends to random non-dead ends
# none of the key events are in dead ends, so we don't need to do gate_before_switch
while len(dead_ends) > 0:
if hasattr(world.multiworld, "re_gen_passthrough"):
if world.using_ut:
break
portal1 = two_plus.pop()
portal2 = dead_ends.pop()
@@ -470,7 +467,7 @@ def pair_portals(world: "TunicWorld", regions: Dict[str, Region]) -> Dict[Portal
# then randomly connect the remaining portals to each other
# every region is accessible, so gate_before_switch is not necessary
while len(two_plus) > 1:
if hasattr(world.multiworld, "re_gen_passthrough"):
if world.using_ut:
break
portal1 = two_plus.pop()
portal2 = two_plus.pop()

View File

@@ -323,7 +323,7 @@ def set_location_rules(world: "TunicWorld") -> None:
# Beneath the Vault
set_rule(world.get_location("Beneath the Fortress - Bridge"),
lambda state: has_melee(state, player) or state.has_any({laurels, fire_wand}, player))
lambda state: has_melee(state, player) or state.has_any((laurels, fire_wand, ice_dagger, gun), player))
set_rule(world.get_location("Beneath the Fortress - Obscured Behind Waterfall"),
lambda state: has_melee(state, player) and has_lantern(state, world))

View File

@@ -106,6 +106,7 @@ class StaticWitnessLogicObj:
"entityType": location_id,
"locationType": None,
"area": current_area,
"order": len(self.ENTITIES_BY_HEX),
}
self.ENTITIES_BY_NAME[self.ENTITIES_BY_HEX[entity_hex]["checkName"]] = self.ENTITIES_BY_HEX[entity_hex]
@@ -186,6 +187,7 @@ class StaticWitnessLogicObj:
"entityType": entity_type,
"locationType": location_type,
"area": current_area,
"order": len(self.ENTITIES_BY_HEX),
}
self.ENTITY_ID_TO_NAME[entity_hex] = full_entity_name

View File

@@ -114,7 +114,7 @@ class WitnessPlayerRegions:
if k not in player_logic.UNREACHABLE_REGIONS
}
event_locations_per_region = defaultdict(list)
event_locations_per_region = defaultdict(dict)
for event_location, event_item_and_entity in player_logic.EVENT_ITEM_PAIRS.items():
region = static_witness_logic.ENTITIES_BY_HEX[event_item_and_entity[1]]["region"]
@@ -122,20 +122,33 @@ class WitnessPlayerRegions:
region_name = "Entry"
else:
region_name = region["name"]
event_locations_per_region[region_name].append(event_location)
order = self.reference_logic.ENTITIES_BY_HEX[event_item_and_entity[1]]["order"]
event_locations_per_region[region_name][event_location] = order
for region_name, region in regions_to_create.items():
locations_for_this_region = [
self.reference_logic.ENTITIES_BY_HEX[panel]["checkName"] for panel in region["entities"]
if self.reference_logic.ENTITIES_BY_HEX[panel]["checkName"]
in self.player_locations.CHECK_LOCATION_TABLE
location_entities_for_this_region = [
self.reference_logic.ENTITIES_BY_HEX[entity] for entity in region["entities"]
]
locations_for_this_region = {
entity["checkName"]: entity["order"] for entity in location_entities_for_this_region
if entity["checkName"] in self.player_locations.CHECK_LOCATION_TABLE
}
locations_for_this_region += event_locations_per_region[region_name]
events = event_locations_per_region[region_name]
locations_for_this_region.update(events)
# First, sort by keys.
locations_for_this_region = dict(sorted(locations_for_this_region.items()))
# Then, sort by game order (values)
locations_for_this_region = dict(sorted(
locations_for_this_region.items(),
key=lambda location_name_and_order: location_name_and_order[1]
))
all_locations = all_locations | set(locations_for_this_region)
new_region = create_region(world, region_name, self.player_locations, locations_for_this_region)
new_region = create_region(world, region_name, self.player_locations, list(locations_for_this_region))
regions_by_name[region_name] = new_region

View File

@@ -176,7 +176,7 @@ class ZorkGrandInquisitorWorld(World):
if start_with_hotspot_items:
item: ZorkGrandInquisitorItems
for item in items_with_tag(ZorkGrandInquisitorTags.HOTSPOT):
for item in sorted(items_with_tag(ZorkGrandInquisitorTags.HOTSPOT), key=lambda item: item.name):
self.multiworld.push_precollected(self.create_item(item.value))
def create_item(self, name: str) -> ZorkGrandInquisitorItem: