Repository: randovania/randovania
Pull request #8270: Randomprime v1.29.0
Author: login: toasterparty
Labels: name: Patcher
description: Pertaining to game-specific modifications, typically via Randovania dependencies
name: dependencies
description: Pull requests that update a dependency file
name: game: Metroid Prime 1
Created: 2025-04-26T15:48:25Z | Merged: 2025-05-13T19:12:47Z

Cherry-picks some changes from when we upgraded to the bugged v1.28.X version



Diff:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f6d17537d3..f407514d7c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -21,6 +21,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 - Fixed: If Metroid Queen is the final boss and you don't have all DNA, the DNA message now appears.
 - Fixed: For tanks with non required main option, the jingle of the tank item is played instead of the jingle of the main object.
 
+### Metroid Prime
+
+- Added: Blast Shield Lock-On preset option
+- Added: Patch to fix Kerry Ann Odem's name in the credits on game version(s) where it is incorrectly spelled
+- Fixed: Main Plaza vault ledge door is now affected by door lock rando
+- Fixed: Elite Research Phazon Elite getting stuck on frame perfect cutscene skip
+- Fixed: Minor music transition fixes
+- Fixed: Chaos option export error when submerging rooms
+- Changed: [PAL] Remove extra door lock in Central Dynamo
+- Changed: [PAL] Load Hunter Metroid in Frost Cave
+
 ## [9.1.1] - 2025-05-03
 
 - Fixed: Duplicating a session now also duplicates the admin settings for Co-op and "Everyone can claim worlds".
diff --git a/pyproject.toml b/pyproject.toml
index 70b3a89bc3..75b72a2d1e 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -95,7 +95,7 @@ exporters = [
     "planets-yapr>=1.0.0",
 
     # Metroid Prime 1
-    "py_randomprime>=1.27.0",
+    "py_randomprime>=1.29.0",
     "random-enemy-attributes>=1.0.3",
 
     # Metroid Prime 1/2
diff --git a/randovania/games/prime1/exporter/patch_data_factory.py b/randovania/games/prime1/exporter/patch_data_factory.py
index bd8a5016b3..f66301dd3b 100644
--- a/randovania/games/prime1/exporter/patch_data_factory.py
+++ b/randovania/games/prime1/exporter/patch_data_factory.py
@@ -802,9 +802,6 @@ def create_game_specific_data(self, randovania_meta: PatcherDataMeta) -> dict:
                     key=lambda n: n.extra["dock_index"],
                 )
                 for node in dock_nodes:
-                    if node.extra.get("exclude_dock_rando", False):
-                        continue
-
                     if self.patches.has_default_weakness(node):
                         continue
 
@@ -999,6 +996,11 @@ def get_random_size(minimum: float, maximum: float) -> float:
         else:
             boss_sizes = {}
 
+        if self.configuration.spring_ball:
+            spring_ball_item = "Morph Ball Bomb"
+        else:
+            spring_ball_item = "Spring Ball"
+
         data: dict = {
             "$schema": "https://randovania.github.io/randomprime/randomprime.schema.json",
             "seed": self.description.get_seed_for_world(self.players_config.player_index),
@@ -1024,7 +1026,7 @@ def get_random_size(minimum: float, maximum: float) -> float:
                 "noDoors": self.configuration.no_doors,
                 "startingRoom": starting_room,
                 "warpToStart": self.configuration.warp_to_start,
-                "springBall": self.configuration.spring_ball,
+                "springBallItem": spring_ball_item,
                 "incineratorDroneConfig": idrone_config,
                 "mazeSeeds": maze_seeds,
                 "nonvariaHeatDamage": not self.configuration.legacy_mode,
@@ -1035,6 +1037,7 @@ def get_random_size(minimum: float, maximum: float) -> float:
                 "multiworldDolPatches": True,
                 "doorOpenMode": "PrimaryBlastShield",
                 "difficultyBehavior": self.configuration.ingame_difficulty.randomprime_value,
+                "blastShieldLockon": self.configuration.blast_shield_lockon,
                 "disableItemLoss": True,  # Item Loss in Frigate
                 "startingItems": starting_items,
                 "etankCapacity": self.configuration.energy_per_tank,
diff --git a/randovania/games/prime1/generator/base_patches_factory.py b/randovania/games/prime1/generator/base_patches_factory.py
index 04dee93389..be23aecd7f 100644
--- a/randovania/games/prime1/generator/base_patches_factory.py
+++ b/randovania/games/prime1/generator/base_patches_factory.py
@@ -33,7 +33,7 @@ def assign_static_dock_weakness(
         dock_weakness: list[tuple[DockNode, DockWeakness]] = []
         power_weak = game.dock_weakness_database.get_by_weakness("door", "Normal Door (Forced)")
 
-        if configuration.main_plaza_door and not configuration.dock_rando.is_enabled():
+        if configuration.main_plaza_door:
             dock_weakness.append(
                 (get_node(nic("Chozo Ruins", "Main Plaza", "Door from Plaza Access"), DockNode), power_weak),
             )
diff --git a/randovania/games/prime1/gui/preset_settings/__init__.py b/randovania/games/prime1/gui/preset_settings/__init__.py
index 9cab449447..390ffe52b7 100644
--- a/randovania/games/prime1/gui/preset_settings/__init__.py
+++ b/randovania/games/prime1/gui/preset_settings/__init__.py
@@ -9,13 +9,13 @@
 
 
 def prime1_preset_tabs(editor: PresetEditor, window_manager: WindowManager) -> list[type[PresetTab]]:
+    from randovania.games.prime1.gui.preset_settings.prime_dock_rando_tab import PresetPrimeDockRando
     from randovania.games.prime1.gui.preset_settings.prime_enemy_stat_randomizer import PresetEnemyAttributeRandomizer
     from randovania.games.prime1.gui.preset_settings.prime_generation_tab import PresetPrimeGeneration
     from randovania.games.prime1.gui.preset_settings.prime_goal_tab import PresetPrimeGoal
     from randovania.games.prime1.gui.preset_settings.prime_patches_chaos import PresetPrimeChaos
     from randovania.games.prime1.gui.preset_settings.prime_patches_qol import PresetPrimeQol
     from randovania.games.prime1.gui.preset_settings.prime_teleporters_tab import PresetTeleportersPrime1
-    from randovania.gui.preset_settings.dock_rando_tab import PresetDockRando
     from randovania.gui.preset_settings.hints_tab import PresetHints
     from randovania.gui.preset_settings.location_pool_tab import PresetLocationPool
     from randovania.gui.preset_settings.metroid_item_pool_tab import MetroidPresetItemPool
@@ -33,7 +33,7 @@ def prime1_preset_tabs(editor: PresetEditor, window_manager: WindowManager) -> l
         PresetPatcherEnergy,
         PresetTeleportersPrime1,
         PresetMetroidStartingArea,
-        PresetDockRando,
+        PresetPrimeDockRando,
         PresetPrimeQol,
         PresetEnemyAttributeRandomizer,
         PresetPrimeChaos,
diff --git a/randovania/games/prime1/gui/preset_settings/prime_dock_rando_tab.py b/randovania/games/prime1/gui/preset_settings/prime_dock_rando_tab.py
new file mode 100644
index 0000000000..7272cb4f3f
--- /dev/null
+++ b/randovania/games/prime1/gui/preset_settings/prime_dock_rando_tab.py
@@ -0,0 +1,93 @@
+from __future__ import annotations
+
+import typing
+from typing import TYPE_CHECKING
+
+from PySide6 import QtWidgets
+
+from randovania.games.prime1.layout.prime_configuration import PrimeConfiguration
+from randovania.gui.lib import signal_handling
+from randovania.gui.preset_settings.dock_rando_tab import PresetDockRando
+from randovania.layout.base.dock_rando_configuration import DockRandoMode
+
+if TYPE_CHECKING:
+    from randovania.game_description.game_description import GameDescription
+    from randovania.gui.lib.window_manager import WindowManager
+    from randovania.interface_common.preset_editor import PresetEditor
+    from randovania.layout.preset import Preset
+
+_CHECKBOX_FIELDS = ["blue_save_doors", "blast_shield_lockon"]
+
+
+class PresetPrimeDockRando(PresetDockRando):
+    def __init__(self, editor: PresetEditor, game_description: GameDescription, window_manager: WindowManager):
+        super().__init__(editor, game_description, window_manager)
+
+        self.changes_box = QtWidgets.QGroupBox()
+        self.changes_box.setTitle("Door Changes")
+        self.changes_layout = QtWidgets.QVBoxLayout(self.changes_box)
+
+        extra_widgets: list[tuple[type[QtWidgets.QCheckBox | QtWidgets.QLabel], str, str]] = [
+            (
+                QtWidgets.QCheckBox,
+                "blue_save_doors_check",
+                "Unlock Save Station Doors",
+            ),
+            (
+                QtWidgets.QLabel,
+                "blue_save_doors_label",
+                "Sets all Save Station doors to blue regardless of door randomization mode",
+            ),
+            (
+                QtWidgets.QCheckBox,
+                "blast_shield_lockon_check",
+                "Enable Blast Shield Lock-On",
+            ),
+            (
+                QtWidgets.QLabel,
+                "blast_shield_lockon_label",
+                "Makes all Blast Shield locks targetable in Combat Visor",
+            ),
+        ]
+
+        # Add each widget
+        for widget_type, attr_name, desc in extra_widgets:
+            setattr(self, attr_name, widget_type())
+            widget = getattr(self, attr_name)
+            widget.setText(desc)
+            if widget_type == QtWidgets.QLabel:
+                assert isinstance(widget, QtWidgets.QLabel)
+                widget.setWordWrap(True)
+
+            self.changes_layout.addWidget(widget)
+
+        # Add the group box
+        self.scroll_area_layout.insertWidget(0, self.changes_box)
+
+        # Checkbox Signals
+        for f in _CHECKBOX_FIELDS:
+            self._add_checkbox_persist_option(getattr(self, f"{f}_check"), f)
+
+    def _on_mode_changed(self, value: DockRandoMode) -> None:
+        super()._on_mode_changed(value)
+        with self._editor as editor:
+            assert isinstance(editor.configuration, PrimeConfiguration)
+            if value == DockRandoMode.VANILLA:
+                editor.set_configuration_field("blast_shield_lockon", False)
+            else:
+                editor.set_configuration_field("blue_save_doors", True)
+                editor.set_configuration_field("blast_shield_lockon", True)
+
+    def _add_checkbox_persist_option(self, check: QtWidgets.QCheckBox, attribute_name: str) -> None:
+        def persist(value: bool) -> None:
+            with self._editor as editor:
+                editor.set_configuration_field(attribute_name, value)
+
+        signal_handling.on_checked(check, persist)
+
+    def on_preset_changed(self, preset: Preset) -> None:
+        super().on_preset_changed(preset)
+        config = preset.configuration
+        assert isinstance(config, PrimeConfiguration)
+        for f in _CHECKBOX_FIELDS:
+            typing.cast("QtWidgets.QCheckBox", getattr(self, f"{f}_check")).setChecked(getattr(config, f))
diff --git a/randovania/games/prime1/gui/preset_settings/prime_patches_qol.py b/randovania/games/prime1/gui/preset_settings/prime_patches_qol.py
index 16d14a83ac..59df5aea8a 100644
--- a/randovania/games/prime1/gui/preset_settings/prime_patches_qol.py
+++ b/randovania/games/prime1/gui/preset_settings/prime_patches_qol.py
@@ -18,7 +18,6 @@
 _FIELDS = [
     "warp_to_start",
     "main_plaza_door",
-    "blue_save_doors",
     "backwards_frigate",
     "backwards_labs",
     "backwards_upper_mines",
diff --git a/randovania/games/prime1/gui/ui_files/preset_prime_qol.ui b/randovania/games/prime1/gui/ui_files/preset_prime_qol.ui
index eabacd11b4..20f3b5751f 100644
--- a/randovania/games/prime1/gui/ui_files/preset_prime_qol.ui
+++ b/randovania/games/prime1/gui/ui_files/preset_prime_qol.ui
@@ -42,9 +42,9 @@
        <property name="geometry">
         <rect>
          <x>0</x>
-         <y>-358</y>
+         <y>0</y>
          <width>487</width>
-         <height>1200</height>
+         <height>976</height>
         </rect>
        </property>
        <layout class="QVBoxLayout" name="scroll_layout">
@@ -62,11 +62,8 @@
         </property>
         <item>
          <spacer name="top_spacer">
-          <property name="orientation">
-           <enum>Qt::Orientation::Vertical</enum>
-          </property>
           <property name="sizeType">
-           <enum>QSizePolicy::Policy::Fixed</enum>
+           <enum>QSizePolicy::Fixed</enum>
           </property>
           <property name="sizeHint" stdset="0">
            <size>
@@ -138,9 +135,6 @@
              <property name="text">
               <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;These quality of life options affect the game world and therefore might affect the logical path the generator expects the player to traverse.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
              </property>
-             <property name="alignment">
-              <set>Qt::AlignmentFlag::AlignLeading|Qt::AlignmentFlag::AlignLeft|Qt::AlignmentFlag::AlignTop</set>
-             </property>
              <property name="wordWrap">
               <bool>true</bool>
              </property>
@@ -166,23 +160,6 @@
              </property>
             </widget>
            </item>
-           <item>
-            <widget class="QCheckBox" name="blue_save_doors_check">
-             <property name="text">
-              <string>Unlock Save Station Doors</string>
-             </property>
-            </widget>
-           </item>
-           <item>
-            <widget class="QLabel" name="blue_save_doors_label">
-             <property name="text">
-              <string>Ensure all Save Station doors are blue doors, even with door lock rando enabled</string>
-             </property>
-             <property name="wordWrap">
-              <bool>true</bool>
-             </property>
-            </widget>
-           </item>
            <item>
             <widget class="QCheckBox" name="backwards_frigate_check">
              <property name="text">
diff --git a/randovania/games/prime1/layout/preset_describer.py b/randovania/games/prime1/layout/preset_describer.py
index f7847cbd2c..ce65504b5b 100644
--- a/randovania/games/prime1/layout/preset_describer.py
+++ b/randovania/games/prime1/layout/preset_describer.py
@@ -138,7 +138,6 @@ def different_xyz_randomization(diff_xyz: bool) -> str | None:
                     "Warp to start": configuration.warp_to_start,
                     "Final bosses removed": configuration.teleporters.skip_final_bosses,
                     "Unlocked Vault door": configuration.main_plaza_door,
-                    "Unlocked Save Station doors": configuration.blue_save_doors,
                     "Phazon Elite without Dynamo": configuration.phazon_elite_without_dynamo,
                 },
                 {
@@ -169,6 +168,10 @@ def different_xyz_randomization(diff_xyz: bool) -> str | None:
                 {
                     ingame_difficulty: ingame_difficulty is not None,
                 },
+                {
+                    "Unlocked Save Station doors": configuration.blue_save_doors,
+                    "Blast Shield Lock-On": configuration.blast_shield_lockon,
+                },
                 {
                     enemy_rando_range_scale: enemy_rando_range_scale is not None,
                     enemy_rando_range_health: enemy_rando_range_health is not None,
diff --git a/randovania/games/prime1/layout/prime_configuration.py b/randovania/games/prime1/layout/prime_configuration.py
index 4f5e9adb58..707c5bb053 100644
--- a/randovania/games/prime1/layout/prime_configuration.py
+++ b/randovania/games/prime1/layout/prime_configuration.py
@@ -119,6 +119,7 @@ class PrimeConfiguration(BaseConfiguration):
     legacy_mode: bool
     qol_cutscenes: LayoutCutsceneMode
     ingame_difficulty: IngameDifficulty
+    blast_shield_lockon: bool
 
     enemy_attributes: EnemyAttributeRandomizer | None
 
diff --git a/randovania/games/prime1/logic_database/Chozo Ruins.json b/randovania/games/prime1/logic_database/Chozo Ruins.json
index 7086da890f..d90bae2b9a 100644
--- a/randovania/games/prime1/logic_database/Chozo Ruins.json	
+++ b/randovania/games/prime1/logic_database/Chozo Ruins.json	
@@ -861,7 +861,6 @@
                     "extra": {
                         "dock_index": 4,
                         "nonstandard": false,
-                        "exclude_dock_rando": true,
                         "world_position": [
                             152.46533,
                             86.464966,
@@ -2026,7 +2025,6 @@
                     "extra": {
                         "dock_index": 0,
                         "nonstandard": false,
-                        "exclude_dock_rando": true,
                         "world_position": [
                             152.46495,
                             86.46671,
diff --git a/randovania/games/prime1/logic_database/Chozo Ruins.txt b/randovania/games/prime1/logic_database/Chozo Ruins.txt
index 9fdeaf95d8..e3f0a90fdd 100644
--- a/randovania/games/prime1/logic_database/Chozo Ruins.txt	
+++ b/randovania/games/prime1/logic_database/Chozo Ruins.txt	
@@ -146,7 +146,6 @@ Extra - aabb: [58.406, -65.29903, -1.3835077, 165.42795, 117.940384, 60.33323]
   * Permanently Locked to Plaza Access/Door to Main Plaza
   * Extra - dock_index: 4
   * Extra - nonstandard: False
-  * Extra - exclude_dock_rando: True
   * Extra - world_position: (152.46533, 86.464966, 22.685888)
   * Extra - world_rotation: (0.0, -0.0, 0.0)
   > Locked Door Ledge
@@ -338,7 +337,6 @@ Extra - aabb: [152.46495, 82.239876, 21.922611, 193.40385, 136.64035, 36.41348]
   * Normal Door to Main Plaza/Door from Plaza Access
   * Extra - dock_index: 0
   * Extra - nonstandard: False
-  * Extra - exclude_dock_rando: True
   * Extra - world_position: (152.46495, 86.46671, 22.685825)
   * Extra - world_rotation: (0.0, 0.0, -179.50708)
   > Door to Vault
diff --git a/randovania/layout/preset_migration.py b/randovania/layout/preset_migration.py
index e5b9c81ffe..d1f3d0f54a 100644
--- a/randovania/layout/preset_migration.py
+++ b/randovania/layout/preset_migration.py
@@ -1163,6 +1163,18 @@ def fix(identifier: dict) -> None:
             fix(it)
 
 
+def _migrate_v103(preset: dict, game: RandovaniaGame) -> None:
+    if game != RandovaniaGame.METROID_PRIME:
+        return
+
+    dlr = preset["configuration"]["dock_rando"]["mode"] != "vanilla"
+    legacy = preset["configuration"]["legacy_mode"]
+
+    preset["configuration"]["blast_shield_lockon"] = dlr and not legacy
+    if dlr and not legacy:
+        preset["configuration"]["blue_save_doors"] = True
+
+
 _MIGRATIONS = [
     _migrate_v1,  # v1.1.1-247-gaf9e4a69
     _migrate_v2,  # v1.2.2-71-g0fbabe91
@@ -1266,6 +1278,7 @@ def fix(identifier: dict) -> None:
     _migrate_v100,  # hints configuration
     _migrate_v101,
     _migrate_v102,  # removal of in_dark_aether
+    _migrate_v103,  # prime1 blast-shield lockon
 ]
 CURRENT_VERSION = migration_lib.get_version(_MIGRATIONS)
 
diff --git a/test/games/prime1/layout/test_preset_describer.py b/test/games/prime1/layout/test_preset_describer.py
index 95c5475664..e2ca84c340 100644
--- a/test/games/prime1/layout/test_preset_describer.py
+++ b/test/games/prime1/layout/test_preset_describer.py
@@ -60,10 +60,11 @@ def test_prime_format_params(use_enemy_attribute_randomizer):
         ],
         "Difficulty": [],
         "Game Changes": [
-            "Warp to start, Unlocked Vault door, Unlocked Save Station doors, Phazon Elite without Dynamo",
+            "Warp to start, Unlocked Vault door, Phazon Elite without Dynamo",
             "53.1% chance of superheated, 28.7% chance of submerged",
             "Allowed backwards: Frigate, Labs, Upper Mines",
             "Damage reduction: Additive",
+            "Unlocked Save Station doors",
         ],
     }
 
diff --git a/test/test_files/patcher_data/prime1/dread_prime1_multiworld/world_2.json b/test/test_files/patcher_data/prime1/dread_prime1_multiworld/world_2.json
index 0bcc7eee7e..c308d3eca2 100644
--- a/test/test_files/patcher_data/prime1/dread_prime1_multiworld/world_2.json
+++ b/test/test_files/patcher_data/prime1/dread_prime1_multiworld/world_2.json
@@ -40,7 +40,7 @@
         "noDoors": false,
         "startingRoom": "Tallon Overworld:Landing Site",
         "warpToStart": true,
-        "springBall": false,
+        "springBallItem": "Spring Ball",
         "incineratorDroneConfig": {
             "eyeWaitInitialRandomTime": 0.0,
             "eyeWaitRandomTime": 0.0,
@@ -62,6 +62,7 @@
         "multiworldDolPatches": true,
         "doorOpenMode": "PrimaryBlastShield",
         "difficultyBehavior": "NormalOnly",
+        "blastShieldLockon": false,
         "disableItemLoss": true,
         "startingItems": {
             "powerBeam": true,
@@ -1834,6 +1835,10 @@
                         "2": {
                             "shieldType": "Blue",
                             "blastShieldType": "Missile"
+                        },
+                        "4": {
+                            "shieldType": "Blue",
+                            "blastShieldType": "None"
                         }
                     }
                 },
diff --git a/test/test_files/patcher_data/prime1/multi-am2r+cs+dread+prime1+prime2+msr/world_3.json b/test/test_files/patcher_data/prime1/multi-am2r+cs+dread+prime1+prime2+msr/world_3.json
index 8f46060de8..71ec9c89ed 100644
--- a/test/test_files/patcher_data/prime1/multi-am2r+cs+dread+prime1+prime2+msr/world_3.json
+++ b/test/test_files/patcher_data/prime1/multi-am2r+cs+dread+prime1+prime2+msr/world_3.json
@@ -36,7 +36,7 @@
         "noDoors": false,
         "startingRoom": "Tallon Overworld:Landing Site",
         "warpToStart": true,
-        "springBall": false,
+        "springBallItem": "Spring Ball",
         "incineratorDroneConfig": {
             "eyeWaitInitialRandomTime": 0.0,
             "eyeWaitRandomTime": 0.0,
@@ -58,6 +58,7 @@
         "multiworldDolPatches": true,
         "doorOpenMode": "PrimaryBlastShield",
         "difficultyBehavior": "NormalOnly",
+        "blastShieldLockon": false,
         "disableItemLoss": true,
         "startingItems": {
             "powerBeam": true,
@@ -1860,6 +1861,10 @@
                         "2": {
                             "shieldType": "Blue",
                             "blastShieldType": "Missile"
+                        },
+                        "4": {
+                            "shieldType": "Blue",
+                            "blastShieldType": "None"
                         }
                     }
                 },
diff --git a/test/test_files/patcher_data/prime1/multi-cs+dread+prime1+prime2/world_2.json b/test/test_files/patcher_data/prime1/multi-cs+dread+prime1+prime2/world_2.json
index 4ddd61754e..43c84b5ada 100644
--- a/test/test_files/patcher_data/prime1/multi-cs+dread+prime1+prime2/world_2.json
+++ b/test/test_files/patcher_data/prime1/multi-cs+dread+prime1+prime2/world_2.json
@@ -36,7 +36,7 @@
         "noDoors": false,
         "startingRoom": "Tallon Overworld:Landing Site",
         "warpToStart": true,
-        "springBall": false,
+        "springBallItem": "Spring Ball",
         "incineratorDroneConfig": {
             "eyeWaitInitialRandomTime": 0.0,
             "eyeWaitRandomTime": 0.0,
@@ -58,6 +58,7 @@
         "multiworldDolPatches": true,
         "doorOpenMode": "PrimaryBlastShield",
         "difficultyBehavior": "NormalOnly",
+        "blastShieldLockon": false,
         "disableItemLoss": true,
         "startingItems": {
             "powerBeam": true,
@@ -1859,6 +1860,10 @@
                         "2": {
                             "shieldType": "Blue",
                             "blastShieldType": "Missile"
+                        },
+                        "4": {
+                            "shieldType": "Blue",
+                            "blastShieldType": "None"
                         }
                     }
                 },
diff --git a/test/test_files/patcher_data/prime1/multi-cs+dread+prime1+prime2/world_6.json b/test/test_files/patcher_data/prime1/multi-cs+dread+prime1+prime2/world_6.json
index df52e89451..4127c93ce1 100644
--- a/test/test_files/patcher_data/prime1/multi-cs+dread+prime1+prime2/world_6.json
+++ b/test/test_files/patcher_data/prime1/multi-cs+dread+prime1+prime2/world_6.json
@@ -36,7 +36,7 @@
         "noDoors": false,
         "startingRoom": "Tallon Overworld:Landing Site",
         "warpToStart": true,
-        "springBall": false,
+        "springBallItem": "Spring Ball",
         "incineratorDroneConfig": {
             "eyeWaitInitialRandomTime": 0.0,
             "eyeWaitRandomTime": 0.0,
@@ -58,6 +58,7 @@
         "multiworldDolPatches": true,
         "doorOpenMode": "PrimaryBlastShield",
         "difficultyBehavior": "NormalOnly",
+        "blastShieldLockon": false,
         "disableItemLoss": true,
         "startingItems": {
             "powerBeam": true,
@@ -1861,6 +1862,10 @@
                         "2": {
                             "shieldType": "Blue",
                             "blastShieldType": "Missile"
+                        },
+                        "4": {
+                            "shieldType": "Blue",
+                            "blastShieldType": "None"
                         }
                     }
                 },
diff --git a/test/test_files/patcher_data/prime1/multi_coop_am2r+bdg+cs+dread+prime1+echoes+msr/world_5.json b/test/test_files/patcher_data/prime1/multi_coop_am2r+bdg+cs+dread+prime1+echoes+msr/world_5.json
index 60e9e5de19..6fbca97a28 100644
--- a/test/test_files/patcher_data/prime1/multi_coop_am2r+bdg+cs+dread+prime1+echoes+msr/world_5.json
+++ b/test/test_files/patcher_data/prime1/multi_coop_am2r+bdg+cs+dread+prime1+echoes+msr/world_5.json
@@ -36,7 +36,7 @@
         "noDoors": false,
         "startingRoom": "Tallon Overworld:Landing Site",
         "warpToStart": true,
-        "springBall": false,
+        "springBallItem": "Spring Ball",
         "incineratorDroneConfig": {
             "eyeWaitInitialRandomTime": 0.0,
             "eyeWaitRandomTime": 0.0,
@@ -58,6 +58,7 @@
         "multiworldDolPatches": true,
         "doorOpenMode": "PrimaryBlastShield",
         "difficultyBehavior": "NormalOnly",
+        "blastShieldLockon": false,
         "disableItemLoss": true,
         "startingItems": {
             "powerBeam": true,
@@ -1861,6 +1862,10 @@
                         "2": {
                             "shieldType": "Blue",
                             "blastShieldType": "Missile"
+                        },
+                        "4": {
+                            "shieldType": "Blue",
+                            "blastShieldType": "None"
                         }
                     }
                 },
diff --git a/test/test_files/patcher_data/prime1/prime1_and_2_multi/world_1.json b/test/test_files/patcher_data/prime1/prime1_and_2_multi/world_1.json
index 72791004de..a765a3d4ae 100644
--- a/test/test_files/patcher_data/prime1/prime1_and_2_multi/world_1.json
+++ b/test/test_files/patcher_data/prime1/prime1_and_2_multi/world_1.json
@@ -40,7 +40,7 @@
         "noDoors": false,
         "startingRoom": "Tallon Overworld:Landing Site",
         "warpToStart": false,
-        "springBall": false,
+        "springBallItem": "Spring Ball",
         "incineratorDroneConfig": {
             "eyeWaitInitialRandomTime": 0.0,
             "eyeWaitRandomTime": 0.0,
@@ -62,6 +62,7 @@
         "multiworldDolPatches": true,
         "doorOpenMode": "PrimaryBlastShield",
         "difficultyBehavior": "NormalOnly",
+        "blastShieldLockon": false,
         "disableItemLoss": true,
         "startingItems": {
             "powerBeam": true,
diff --git a/test/test_files/patcher_data/prime1/prime1_crazy_seed/world_1.json b/test/test_files/patcher_data/prime1/prime1_crazy_seed/world_1.json
index bba8de0653..0b1a14a13d 100644
--- a/test/test_files/patcher_data/prime1/prime1_crazy_seed/world_1.json
+++ b/test/test_files/patcher_data/prime1/prime1_crazy_seed/world_1.json
@@ -40,7 +40,7 @@
         "noDoors": true,
         "startingRoom": "Magmoor Caverns:Shore Tunnel",
         "warpToStart": true,
-        "springBall": true,
+        "springBallItem": "Morph Ball Bomb",
         "incineratorDroneConfig": {
             "eyeWaitInitialRandomTime": 0.0,
             "eyeWaitRandomTime": 0.0,
@@ -62,6 +62,7 @@
         "multiworldDolPatches": true,
         "doorOpenMode": "PrimaryBlastShield",
         "difficultyBehavior": "NormalOnly",
+        "blastShieldLockon": true,
         "disableItemLoss": true,
         "startingItems": {
             "powerBeam": true,
@@ -9591,6 +9592,14 @@
                                 "dockNum": 0
                             }
                         },
+                        "4": {
+                            "shieldType": "Blue",
+                            "blastShieldType": "None",
+                            "destination": {
+                                "roomName": "Sunchamber Lobby",
+                                "dockNum": 1
+                            }
+                        },
                         "0": {
                             "destination": {
                                 "roomName": "Energy Core Access",
@@ -9609,12 +9618,6 @@
                                 "dockNum": 0
                             }
                         },
-                        "4": {
-                            "destination": {
-                                "roomName": "Sunchamber Lobby",
-                                "dockNum": 1
-                            }
-                        },
                         "5": {
                             "destination": {
                                 "roomName": "Watery Hall Access",
diff --git a/test/test_files/patcher_data/prime1/prime1_crazy_seed_one_way_door/world_1.json b/test/test_files/patcher_data/prime1/prime1_crazy_seed_one_way_door/world_1.json
index f5403c0ec7..6f58d03ba9 100644
--- a/test/test_files/patcher_data/prime1/prime1_crazy_seed_one_way_door/world_1.json
+++ b/test/test_files/patcher_data/prime1/prime1_crazy_seed_one_way_door/world_1.json
@@ -56,7 +56,7 @@
         "noDoors": false,
         "startingRoom": "Tallon Overworld:Tallon Canyon",
         "warpToStart": true,
-        "springBall": true,
+        "springBallItem": "Morph Ball Bomb",
         "incineratorDroneConfig": null,
         "mazeSeeds": null,
         "nonvariaHeatDamage": false,
@@ -67,6 +67,7 @@
         "multiworldDolPatches": true,
         "doorOpenMode": "PrimaryBlastShield",
         "difficultyBehavior": "HardOnly",
+        "blastShieldLockon": false,
         "disableItemLoss": true,
         "startingItems": {
             "powerBeam": true,
diff --git a/test/test_files/patcher_data/prime1/prime1_refills/world_1.json b/test/test_files/patcher_data/prime1/prime1_refills/world_1.json
index 8e79488a43..d38f1837dc 100644
--- a/test/test_files/patcher_data/prime1/prime1_refills/world_1.json
+++ b/test/test_files/patcher_data/prime1/prime1_refills/world_1.json
@@ -36,7 +36,7 @@
         "noDoors": false,
         "startingRoom": "Tallon Overworld:Landing Site",
         "warpToStart": true,
-        "springBall": false,
+        "springBallItem": "Spring Ball",
         "incineratorDroneConfig": {
             "eyeWaitInitialRandomTime": 0.0,
             "eyeWaitRandomTime": 0.0,
@@ -58,6 +58,7 @@
         "multiworldDolPatches": true,
         "doorOpenMode": "PrimaryBlastShield",
         "difficultyBehavior": "NormalOnly",
+        "blastShieldLockon": false,
         "disableItemLoss": true,
         "startingItems": {
             "powerBeam": true,
@@ -1856,6 +1857,10 @@
                         "2": {
                             "shieldType": "Blue",
                             "blastShieldType": "Missile"
+                        },
+                        "4": {
+                            "shieldType": "Blue",
+                            "blastShieldType": "None"
                         }
                     }
                 },
diff --git a/uv.lock b/uv.lock
index 0b4a1fb71e..e6c82928cd 100644
--- a/uv.lock
+++ b/uv.lock
@@ -1521,13 +1521,13 @@ wheels = [
 
 [[package]]
 name = "py-randomprime"
-version = "1.27.0"
+version = "1.29.0"
 source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/55/39/ed088bbde0c8b60c5e7433b52be4cd5ffa25a774d7ef29d56f23d5c2aee0/py_randomprime-1.27.0.tar.gz", hash = "sha256:f78df7df81d0c5c56d84dfe92b4aa40aee39266c75cbc07ff59bc3b4ddcc4216", size = 10588310 }
+sdist = { url = "https://files.pythonhosted.org/packages/f3/f2/559e6145a4e9e35c74088276ed2bde3155cdf3c1fe3a382c79acef5f2e1a/py_randomprime-1.29.0.tar.gz", hash = "sha256:eea99141e202abc6f093043b864b859c03a562da5d177a4b8fdb4c633b9a38fc", size = 10592593 }
 wheels = [
-    { url = "https://files.pythonhosted.org/packages/d2/a8/b2b1db02dd56f4ec75c3d26b4aa17dfa42a5b9abe1ba64e107a589e05570/py_randomprime-1.27.0-cp38-abi3-macosx_10_9_x86_64.whl", hash = "sha256:0d1278ee21ab974bff7b734cb99d59c9b4b2e6cf3c158cc786bf18b82805ffeb", size = 3308887 },
-    { url = "https://files.pythonhosted.org/packages/09/13/ba7974356f6fc27ed97ae37f33e116026193e0fada2ee4c0f3fbb33be08e/py_randomprime-1.27.0-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2f47b79b29223b2b420c9aadbd2bb840206c8d127981000a54ce36d140509ff2", size = 3568958 },
-    { url = "https://files.pythonhosted.org/packages/a2/77/e90e0497ae27eb898b5797ebaafb85029c63fad2a65f1ff3174d66bb7b57/py_randomprime-1.27.0-cp38-abi3-win_amd64.whl", hash = "sha256:9d50cc23b668584538dac8c798a8b8f311e462ac177169ca380fbec393c405e6", size = 3092804 },
+    { url = "https://files.pythonhosted.org/packages/7e/58/20a7353c851e867c06a0693fa2aabdadcc87b14dd8aac1760e9b871ee254/py_randomprime-1.29.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:e21fa7877a05b6272ea501d136b954ef1c267ccd263f30a7f1e76108077f072d", size = 3368260 },
+    { url = "https://files.pythonhosted.org/packages/bc/61/84016e84beb137f0472a7291a8fdcafeabb2d77bfa89b3699eb9df78902f/py_randomprime-1.29.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a78f5be653572a4c826b7817438839a041ce6d8d813f33c242cc2ab53ca1eecc", size = 3595968 },
+    { url = "https://files.pythonhosted.org/packages/b8/86/3a84230437c7b4db4cff38d96cceddad64b8eeda801c5b2ea79e3afe62e6/py_randomprime-1.29.0-cp39-abi3-win_amd64.whl", hash = "sha256:71524f7f518445510d5384fb2a7f9a36fc34a8363219b9682be0d45727191081", size = 3098854 },
 ]
 
 [[package]]
@@ -2059,7 +2059,7 @@ requires-dist = [
     { name = "ppc-asm", marker = "extra == 'exporters'", specifier = ">=1.2.1" },
     { name = "prometheus-flask-exporter", marker = "extra == 'server'" },
     { name = "py-cord", marker = "extra == 'server'", specifier = ">=2.3.1" },
-    { name = "py-randomprime", marker = "extra == 'exporters'", specifier = ">=1.27.0" },
+    { name = "py-randomprime", marker = "extra == 'exporters'", specifier = ">=1.29.0" },
     { name = "pyqtdarktheme-fork", marker = "extra == 'gui'", specifier = ">=2.3.0" },
     { name = "pyside6-essentials", marker = "extra == 'gui'", specifier = ">=6.5" },
     { name = "pytest", marker = "extra == 'test'" },


Reviews:
user: login: ThanatosGit
body: Cannot say anything about the game specific stuff but otherwise lgtm.
Can you merge main? I am pretty sure that the changelog will not be inserted in the right place.
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-13T13:44:36Z
user: login: ThanatosGit
state: DISMISSED
author_association: CONTRIBUTOR
submitted_at: 2025-05-13T17:17:12Z
user: login: Miepee
body: i would like one another person approving on prime1 specifics actually.
because i'm not a fan ux wise of:
- the Blast Shield Lock-On preset option existing at all
- the checkbox being changed by a completely different setting
state: DISMISSED
author_association: CONTRIBUTOR
submitted_at: 2025-05-13T17:38:29Z
user: login: henriquegemignani
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-13T17:43:54Z
user: login: henriquegemignani
body: Code-wide it looks fine. not 100% happy with the `getattr` but we already do that a bunch and I don't have a much better idea anyway
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-13T17:56:31Z
user: login: henriquegemignani
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-13T17:58:23Z
user: login: toasterparty
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-13T18:08:03Z
user: login: henriquegemignani
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-13T19:03:35Z

Review comments:
diff_hunk: @@ -72,6 +72,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 ### Metroid Prime
 
+- Added: Blast Shield Lock-On preset option
+- Added: Patch to fix Kerry Ann Odem's name in the credits on game version(s) where it is incorrectly spelled
+- Fixed: Main Plaza vault ledge door is now affected by door lock rando
+- Fixed: Elite Research Phazon Elite getting stuck on frame perfect cutscene skip
+- Fixed: Minor music transition fixes
+- Fixed: Chaos option export error when submerging rooms
+- Changed: [PAL] Remove extra door lock in Central Dynamo
+- Changed: [PAL] Load Hunter Metroid in Frost Cave
path: CHANGELOG.md
user: login: ThanatosGit
body: Now you merged main but as I expected it is under the headline of the already released version 9.1.0 :p
created_at: 2025-05-13T17:17:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -62,11 +62,8 @@
         </property>
         <item>
          <spacer name="top_spacer">
-          <property name="orientation">
-           <enum>Qt::Orientation::Vertical</enum>
-          </property>
           <property name="sizeType">
-           <enum>QSizePolicy::Policy::Fixed</enum>
+           <enum>QSizePolicy::Fixed</enum>
path: randovania/games/prime1/gui/ui_files/preset_prime_qol.ui
user: login: henriquegemignani
body: Seems like your Qt Creator is outdated.
created_at: 2025-05-13T17:43:54Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,85 @@
+from __future__ import annotations
+
+import typing
+from typing import TYPE_CHECKING
+
+from PySide6 import QtWidgets
+
+from randovania.games.prime1.layout.prime_configuration import PrimeConfiguration
+from randovania.gui.lib import signal_handling
+from randovania.gui.preset_settings.dock_rando_tab import PresetDockRando
+from randovania.layout.base.dock_rando_configuration import DockRandoMode
+
+if TYPE_CHECKING:
+    from randovania.game_description.game_description import GameDescription
+    from randovania.gui.lib.window_manager import WindowManager
+    from randovania.interface_common.preset_editor import PresetEditor
+    from randovania.layout.preset import Preset
+
+_CHECKBOX_FIELDS = ["blue_save_doors", "blast_shield_lockon"]
+
+
+class PresetPrimeDockRando(PresetDockRando):
+    def __init__(self, editor: PresetEditor, game_description: GameDescription, window_manager: WindowManager):
+        super().__init__(editor, game_description, window_manager)
+
+        self.changes_box = QtWidgets.QGroupBox()
+        self.changes_box.setTitle("Door Changes")
+        self.changes_layout = QtWidgets.QVBoxLayout(self.changes_box)
+
+        extra_widgets: list[tuple[type[QtWidgets.QCheckBox | QtWidgets.QLabel], str, str]] = [
+            (QtWidgets.QCheckBox, "blue_save_doors_check", "Unlock Save Station Doors"),
path: randovania/games/prime1/gui/preset_settings/prime_dock_rando_tab.py
user: login: henriquegemignani
body: Can you add a `,` after the last string in this line, so that all three are formatted the same?
created_at: 2025-05-13T17:55:30Z
author_association: MEMBER
diff_hunk: @@ -1163,6 +1163,16 @@ def fix(identifier: dict) -> None:
             fix(it)
 
 
+def _migrate_v103(preset: dict, game: RandovaniaGame) -> None:
+    if game != RandovaniaGame.METROID_PRIME:
+        return
+
+    dlr = preset["configuration"]["dock_rando"]["mode"] != "vanilla"
+    legacy = preset["configuration"]["legacy_mode"]
+    preset["configuration"]["blast_shield_lockon"] = dlr and not legacy
+    preset["configuration"]["blue_save_doors"] = dlr and not legacy
path: randovania/layout/preset_migration.py
user: login: henriquegemignani
body: With this, a preset that has DLR disabled but blue save doors on will be migrated to disabled. Is this intended?
created_at: 2025-05-13T17:58:23Z
author_association: MEMBER
diff_hunk: @@ -62,11 +62,8 @@
         </property>
         <item>
          <spacer name="top_spacer">
-          <property name="orientation">
-           <enum>Qt::Orientation::Vertical</enum>
-          </property>
           <property name="sizeType">
-           <enum>QSizePolicy::Policy::Fixed</enum>
+           <enum>QSizePolicy::Fixed</enum>
path: randovania/games/prime1/gui/ui_files/preset_prime_qol.ui
user: login: toasterparty
body: 16.0.1 installed from GitHub. Not sure how to fix
created_at: 2025-05-13T18:08:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -62,11 +62,8 @@
         </property>
         <item>
          <spacer name="top_spacer">
-          <property name="orientation">
-           <enum>Qt::Orientation::Vertical</enum>
-          </property>
           <property name="sizeType">
-           <enum>QSizePolicy::Policy::Fixed</enum>
+           <enum>QSizePolicy::Fixed</enum>
path: randovania/games/prime1/gui/ui_files/preset_prime_qol.ui
user: login: henriquegemignani
body: if it's updated then leave it like that
created_at: 2025-05-13T19:03:34Z
author_association: MEMBER

PR comments:
user: login: codecov[bot]
created_at: 2025-04-26T15:56:54Z
body: ## [Codecov](https://app.codecov.io/gh/randovania/randovania/pull/8270?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=randovania) Report
Attention: Patch coverage is `88.33333%` with `7 lines` in your changes missing coverage. Please review.
> Project coverage is 85.82%. Comparing base [(`203e590`)](https://app.codecov.io/gh/randovania/randovania/commit/203e590a8a1a8df2f98293db40cee37e4265abba?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=randovania) to head [(`7fc023f`)](https://app.codecov.io/gh/randovania/randovania/commit/7fc023fd46d8975d2a2c7ae7129cc876176a9135?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=randovania).
> Report is 17 commits behind head on main.

| [Files with missing lines](https://app.codecov.io/gh/randovania/randovania/pull/8270?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=randovania) | Patch % | Lines |
|---|---|---|
| [...prime1/gui/preset\_settings/prime\_dock\_rando\_tab.py](https://app.codecov.io/gh/randovania/randovania/pull/8270?src=pr&el=tree&filepath=randovania%2Fgames%2Fprime1%2Fgui%2Fpreset_settings%2Fprime_dock_rando_tab.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=randovania#diff-cmFuZG92YW5pYS9nYW1lcy9wcmltZTEvZ3VpL3ByZXNldF9zZXR0aW5ncy9wcmltZV9kb2NrX3JhbmRvX3RhYi5weQ==) | 84.78% | [7 Missing :warning: ](https://app.codecov.io/gh/randovania/randovania/pull/8270?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=randovania) |

<details><summary>Additional details and impacted files</summary>


```diff
@@           Coverage Diff           @@
##             main    #8270   +/-   ##
=======================================
  Coverage   85.82%   85.82%           
=======================================
  Files         727      728    +1     
  Lines       46492    46548   +56     
  Branches     5725     5732    +7     
=======================================
+ Hits        39903    39952   +49     
- Misses       5315     5322    +7     
  Partials     1274     1274           
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/randovania/randovania/pull/8270?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=randovania).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=randovania).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>
author_association: NONE
user: login: toasterparty
created_at: 2025-05-13T17:37:38Z
body: I'm self-approving on the prime1-specifics. Go ahead and merge if the python holds up
author_association: CONTRIBUTOR
user: login: Miepee
created_at: 2025-05-13T17:39:09Z
body: CC @JeffGainsNGames @Uncle-Reggie @duncathan 
author_association: CONTRIBUTOR
user: login: Uncle-Reggie
created_at: 2025-05-13T17:49:57Z
body: > i would like one another person approving on prime1 specifics actually. because i'm not a fan ux wise of:
> 
>     * the Blast Shield Lock-On preset option existing at all
> 
>     * the checkbox being changed by a completely different setting

I don't see the issue in the setting being force-enabled as long as it is mentioned that it does so, preferably in a tooltip. The settings being disabled by that option I do find to be somewhat unintuitive.

I also think this is a divisive enough option to have a dedicated checkbox, or, force to always enabled behind the scenes and disable it in legacy mode.
author_association: CONTRIBUTOR
user: login: duncathan
created_at: 2025-05-13T18:28:14Z
body: > i would like one another person approving on prime1 specifics actually.
> because i'm not a fan ux wise of:
> - the Blast Shield Lock-On preset option existing at all
> - the checkbox being changed by a completely different setting

in my experience the latter practice leads to issues and user confusion. Cave Story does this in one or two spots and it's always been problematic. I strongly encourage finding another way to achieve whatever it is you're trying to do here
author_association: CONTRIBUTOR
user: login: Uncle-Reggie
created_at: 2025-05-13T18:33:27Z
body: > I don't see the issue in the setting being force-enabled as long as it is mentioned that it does so, preferably in a tooltip. The settings being disabled by that option I do find to be somewhat unintuitive.
> 
> I also think this is a divisive enough option to have a dedicated checkbox, or, force to always enabled behind the scenes and disable it in legacy mode.

Actually, having seen the windows being interacted with, I do not take issue with any of the decisions being made here and support them as they are. The setting needs to be an option, and the defaults associated with DLR are in good taste with how the two settings primarily go hand-in-hand.
author_association: CONTRIBUTOR