diff --git a/QSB/DeathSync/Patches/DeathPatches.cs b/QSB/DeathSync/Patches/DeathPatches.cs index a0e8e258..a3a494c9 100644 --- a/QSB/DeathSync/Patches/DeathPatches.cs +++ b/QSB/DeathSync/Patches/DeathPatches.cs @@ -150,50 +150,121 @@ public class DeathPatches : QSBPatch [HarmonyPrefix] [HarmonyPatch(typeof(DeathManager), nameof(DeathManager.KillPlayer))] - public static bool DeathManager_KillPlayer_Prefix(DeathType deathType) + private static bool DeathManager_KillPlayer(DeathManager __instance, DeathType deathType) { - if (RespawnOnDeath.Instance == null) - { - return true; - } - - if (RespawnOnDeath.Instance.AllowedDeathTypes.Contains(deathType)) - { - return true; - } - - if (QSBPlayerManager.LocalPlayer.IsDead) - { - return false; - } - - var deadPlayersCount = QSBPlayerManager.PlayerList.Count(x => x.IsDead); - - if (deadPlayersCount == QSBPlayerManager.PlayerList.Count - 1) - { - new EndLoopMessage().Send(); - return true; - } - - RespawnOnDeath.Instance.ResetPlayer(); + Original(__instance, deathType); return false; - } - [HarmonyPostfix] - [HarmonyPatch(typeof(DeathManager), nameof(DeathManager.KillPlayer))] - public static void DeathManager_KillPlayer_Postfix(DeathType deathType) - { - if (QSBPlayerManager.LocalPlayer.IsDead) + static void Original(DeathManager @this, DeathType deathType) { - return; + @this._fakeMeditationDeath = false; + if (deathType == DeathType.Meditation && @this.CheckShouldWakeInDreamWorld()) + { + @this._fakeMeditationDeath = true; + OWInput.ChangeInputMode(InputMode.None); + ReticleController.Hide(); + Locator.GetPromptManager().SetPromptsVisible(false); + GlobalMessenger.FireEvent("FakePlayerMeditationDeath"); + return; + } + + if (deathType == DeathType.DreamExplosion) + { + Achievements.Earn(Achievements.Type.EARLY_ADOPTER); + } + + if (PlayerState.InDreamWorld() && deathType != DeathType.Dream && deathType != DeathType.DreamExplosion && deathType != DeathType.Supernova && deathType != DeathType.TimeLoop && deathType != DeathType.Meditation) + { + Locator.GetDreamWorldController().ExitDreamWorld(deathType); + return; + } + + if (!@this._isDying) + { + if (@this._invincible && deathType != DeathType.Supernova && deathType != DeathType.BigBang && deathType != DeathType.Meditation && deathType != DeathType.TimeLoop && deathType != DeathType.BlackHole) + { + return; + } + + if (!Custom(deathType)) + { + return; + } + + if (!TimeLoopCoreController.ParadoxExists()) + { + var component = Locator.GetPlayerBody().GetComponent(); + if ((deathType == DeathType.TimeLoop || deathType == DeathType.Supernova) && component.GetTotalDamageThisLoop() > 1000f) + { + Achievements.Earn(Achievements.Type.DIEHARD); + PlayerData.SetPersistentCondition("THERE_IS_BUT_VOID", true); + } + + if ((TimeLoop.GetLoopCount() != 1 && TimeLoop.GetSecondsElapsed() < 60f || TimeLoop.GetLoopCount() == 1 && Time.timeSinceLevelLoad < 60f && !TimeLoop.IsTimeFlowing()) && deathType != DeathType.Meditation && LoadManager.GetCurrentScene() == OWScene.SolarSystem) + { + Achievements.Earn(Achievements.Type.GONE_IN_60_SECONDS); + } + + if (TimeLoop.GetLoopCount() > 1) + { + Achievements.SetHeroStat(Achievements.HeroStat.TIMELOOP_COUNT, (uint)(TimeLoop.GetLoopCount() - 1)); + if (deathType == DeathType.TimeLoop || deathType == DeathType.BigBang || deathType == DeathType.Supernova) + { + PlayerData.CompletedFullTimeLoop(); + } + } + + if (deathType == DeathType.Supernova && !PlayerData.GetPersistentCondition("KILLED_BY_SUPERNOVA_AND_KNOWS_IT") && PlayerData.GetFullTimeLoopsCompleted() > 2U && PlayerData.GetPersistentCondition("HAS_SEEN_SUN_EXPLODE")) + { + PlayerData.SetPersistentCondition("KILLED_BY_SUPERNOVA_AND_KNOWS_IT", true); + MonoBehaviour.print("KILLED_BY_SUPERNOVA_AND_KNOWS_IT"); + } + } + + @this._isDying = true; + @this._deathType = deathType; + MonoBehaviour.print("Player was killed by " + deathType); + Locator.GetPauseCommandListener().AddPauseCommandLock(); + PlayerData.SetLastDeathType(deathType); + GlobalMessenger.FireEvent("PlayerDeath", deathType); + } } - QSBPlayerManager.LocalPlayer.IsDead = true; - new PlayerDeathMessage(deathType).Send(); - - if (PlayerAttachWatcher.Current) + static bool Custom(DeathType deathType) { - PlayerAttachWatcher.Current.DetachPlayer(); + if (RespawnOnDeath.Instance == null) + { + return true; + } + + if (RespawnOnDeath.Instance.AllowedDeathTypes.Contains(deathType)) + { + return true; + } + + if (QSBPlayerManager.LocalPlayer.IsDead) + { + return false; + } + + var deadPlayersCount = QSBPlayerManager.PlayerList.Count(x => x.IsDead); + if (deadPlayersCount == QSBPlayerManager.PlayerList.Count - 1) + { + new EndLoopMessage().Send(); + return true; + } + + RespawnOnDeath.Instance.ResetPlayer(); + + QSBPlayerManager.LocalPlayer.IsDead = true; + new PlayerDeathMessage(deathType).Send(); + + if (PlayerAttachWatcher.Current) + { + PlayerAttachWatcher.Current.DetachPlayer(); + } + + return false; } } diff --git a/QSB/EchoesOfTheEye/DreamRafts/DreamRaftManager.cs b/QSB/EchoesOfTheEye/DreamRafts/DreamRaftManager.cs new file mode 100644 index 00000000..3d6d834f --- /dev/null +++ b/QSB/EchoesOfTheEye/DreamRafts/DreamRaftManager.cs @@ -0,0 +1,21 @@ +using Cysharp.Threading.Tasks; +using QSB.EchoesOfTheEye.DreamRafts.WorldObjects; +using QSB.WorldSync; +using System.Threading; + +namespace QSB.EchoesOfTheEye.DreamRafts; + +public class DreamRaftManager : WorldObjectManager +{ + public override WorldObjectScene WorldObjectScene => WorldObjectScene.SolarSystem; + public override bool DlcOnly => true; + + public override async UniTask BuildWorldObjects(OWScene scene, CancellationToken ct) + { + QSBWorldSync.Init(); + QSBWorldSync.Init(); + + QSBWorldSync.Init(); + QSBWorldSync.Init(); + } +} diff --git a/QSB/EchoesOfTheEye/DreamRafts/Messages/ExtinguishMessage.cs b/QSB/EchoesOfTheEye/DreamRafts/Messages/ExtinguishMessage.cs new file mode 100644 index 00000000..20c45823 --- /dev/null +++ b/QSB/EchoesOfTheEye/DreamRafts/Messages/ExtinguishMessage.cs @@ -0,0 +1,11 @@ +using QSB.EchoesOfTheEye.DreamRafts.WorldObjects; +using QSB.Messaging; +using QSB.Patches; + +namespace QSB.EchoesOfTheEye.DreamRafts.Messages; + +public class ExtinguishMessage : QSBWorldObjectMessage +{ + public override void OnReceiveRemote() => + QSBPatch.RemoteCall(() => WorldObject.AttachedObject.SetVisible(false)); +} \ No newline at end of file diff --git a/QSB/EchoesOfTheEye/DreamRafts/Messages/SpawnRaftMessage.cs b/QSB/EchoesOfTheEye/DreamRafts/Messages/SpawnRaftMessage.cs new file mode 100644 index 00000000..2def799a --- /dev/null +++ b/QSB/EchoesOfTheEye/DreamRafts/Messages/SpawnRaftMessage.cs @@ -0,0 +1,11 @@ +using QSB.EchoesOfTheEye.DreamRafts.WorldObjects; +using QSB.Messaging; +using QSB.Patches; + +namespace QSB.EchoesOfTheEye.DreamRafts.Messages; + +public class SpawnRaftMessage : QSBWorldObjectMessage +{ + public override void OnReceiveRemote() => + QSBPatch.RemoteCall(WorldObject.AttachedObject.RespawnRaft); +} diff --git a/QSB/EchoesOfTheEye/DreamRafts/Patches/DreamRaftPatches.cs b/QSB/EchoesOfTheEye/DreamRafts/Patches/DreamRaftPatches.cs new file mode 100644 index 00000000..65fe5cd8 --- /dev/null +++ b/QSB/EchoesOfTheEye/DreamRafts/Patches/DreamRaftPatches.cs @@ -0,0 +1,56 @@ +using HarmonyLib; +using QSB.EchoesOfTheEye.DreamRafts.Messages; +using QSB.EchoesOfTheEye.DreamRafts.WorldObjects; +using QSB.Messaging; +using QSB.Patches; +using QSB.WorldSync; +using System.Linq; + +namespace QSB.EchoesOfTheEye.DreamRafts.Patches; + +public class DreamRaftPatches : QSBPatch +{ + public override QSBPatchTypes Type => QSBPatchTypes.OnClientConnect; + + [HarmonyPrefix] + [HarmonyPatch(typeof(DreamRaftProjector), nameof(DreamRaftProjector.SpawnRaft))] + private static void SpawnRaft(DreamRaftProjector __instance) + { + if (Remote) + { + return; + } + + if (!QSBWorldSync.AllObjectsReady) + { + return; + } + + __instance.GetWorldObject() + .SendMessage(new SpawnRaftMessage()); + } + + [HarmonyPrefix] + [HarmonyPatch(typeof(DreamRaftProjection), nameof(DreamRaftProjection.OnCandleLitStateChanged))] + private static bool OnCandleLitStateChanged(DreamRaftProjection __instance) + { + if (!__instance._visible) + { + return false; + } + + if (__instance._candles.Any(x => x.IsLit())) + { + return false; + } + + __instance.SetVisible(false); + if (!Remote && QSBWorldSync.AllObjectsReady) + { + __instance.GetWorldObject() + .SendMessage(new ExtinguishMessage()); + } + + return false; + } +} diff --git a/QSB/EchoesOfTheEye/DreamRafts/WorldObjects/QSBDreamRaft.cs b/QSB/EchoesOfTheEye/DreamRafts/WorldObjects/QSBDreamRaft.cs new file mode 100644 index 00000000..56fdd832 --- /dev/null +++ b/QSB/EchoesOfTheEye/DreamRafts/WorldObjects/QSBDreamRaft.cs @@ -0,0 +1,13 @@ +using QSB.EchoesOfTheEye.RaftSync.TransformSync; +using QSB.Utility.LinkedWorldObject; +using UnityEngine; + +namespace QSB.EchoesOfTheEye.DreamRafts.WorldObjects; + +public class QSBDreamRaft : LinkedWorldObject +{ + public override void SendInitialState(uint to) { } + + protected override GameObject NetworkObjectPrefab => QSBNetworkManager.singleton.RaftPrefab; + protected override bool SpawnWithServerAuthority => false; +} diff --git a/QSB/EchoesOfTheEye/DreamRafts/WorldObjects/QSBDreamRaftProjection.cs b/QSB/EchoesOfTheEye/DreamRafts/WorldObjects/QSBDreamRaftProjection.cs new file mode 100644 index 00000000..06e34f53 --- /dev/null +++ b/QSB/EchoesOfTheEye/DreamRafts/WorldObjects/QSBDreamRaftProjection.cs @@ -0,0 +1,8 @@ +using QSB.WorldSync; + +namespace QSB.EchoesOfTheEye.DreamRafts.WorldObjects; + +public class QSBDreamRaftProjection : WorldObject +{ + public override void SendInitialState(uint to) { } +} diff --git a/QSB/EchoesOfTheEye/DreamRafts/WorldObjects/QSBDreamRaftProjector.cs b/QSB/EchoesOfTheEye/DreamRafts/WorldObjects/QSBDreamRaftProjector.cs new file mode 100644 index 00000000..ccdafdf2 --- /dev/null +++ b/QSB/EchoesOfTheEye/DreamRafts/WorldObjects/QSBDreamRaftProjector.cs @@ -0,0 +1,16 @@ +using QSB.EchoesOfTheEye.DreamRafts.Messages; +using QSB.Messaging; +using QSB.WorldSync; + +namespace QSB.EchoesOfTheEye.DreamRafts.WorldObjects; + +public class QSBDreamRaftProjector : WorldObject +{ + public override void SendInitialState(uint to) + { + if (AttachedObject._lit) + { + this.SendMessage(new SpawnRaftMessage()); + } + } +} diff --git a/QSB/EchoesOfTheEye/DreamRafts/WorldObjects/QSBSealRaft.cs b/QSB/EchoesOfTheEye/DreamRafts/WorldObjects/QSBSealRaft.cs new file mode 100644 index 00000000..6f5e5f59 --- /dev/null +++ b/QSB/EchoesOfTheEye/DreamRafts/WorldObjects/QSBSealRaft.cs @@ -0,0 +1,13 @@ +using QSB.EchoesOfTheEye.RaftSync.TransformSync; +using QSB.Utility.LinkedWorldObject; +using UnityEngine; + +namespace QSB.EchoesOfTheEye.DreamRafts.WorldObjects; + +public class QSBSealRaft : LinkedWorldObject +{ + public override void SendInitialState(uint to) { } + + protected override GameObject NetworkObjectPrefab => QSBNetworkManager.singleton.RaftPrefab; + protected override bool SpawnWithServerAuthority => false; +} diff --git a/QSB/EchoesOfTheEye/DreamWorld/Messages/EnterDreamWorldMessage.cs b/QSB/EchoesOfTheEye/DreamWorld/Messages/EnterDreamWorldMessage.cs index f21ada43..b85f1c36 100644 --- a/QSB/EchoesOfTheEye/DreamWorld/Messages/EnterDreamWorldMessage.cs +++ b/QSB/EchoesOfTheEye/DreamWorld/Messages/EnterDreamWorldMessage.cs @@ -28,6 +28,8 @@ internal class EnterDreamWorldMessage : QSBWorldObjectMessage OnReceiveRemote(); + public override void OnReceiveRemote() { var player = QSBPlayerManager.GetPlayer(From); diff --git a/QSB/EchoesOfTheEye/DreamWorld/Messages/ExitDreamWorldMessage.cs b/QSB/EchoesOfTheEye/DreamWorld/Messages/ExitDreamWorldMessage.cs index d247e510..ba575bdb 100644 --- a/QSB/EchoesOfTheEye/DreamWorld/Messages/ExitDreamWorldMessage.cs +++ b/QSB/EchoesOfTheEye/DreamWorld/Messages/ExitDreamWorldMessage.cs @@ -22,6 +22,8 @@ internal class ExitDreamWorldMessage : QSBMessage }); } + public override void OnReceiveLocal() => OnReceiveRemote(); + public override void OnReceiveRemote() { var player = QSBPlayerManager.GetPlayer(From); diff --git a/QSB/EchoesOfTheEye/RaftSync/TransformSync/RaftTransformSync.cs b/QSB/EchoesOfTheEye/RaftSync/TransformSync/RaftTransformSync.cs index 36abd9ab..cf03e6b3 100644 --- a/QSB/EchoesOfTheEye/RaftSync/TransformSync/RaftTransformSync.cs +++ b/QSB/EchoesOfTheEye/RaftSync/TransformSync/RaftTransformSync.cs @@ -1,9 +1,9 @@ using QSB.AuthoritySync; -using QSB.EchoesOfTheEye.RaftSync.WorldObjects; using QSB.Syncs.Unsectored.Rigidbodies; using QSB.Utility; using QSB.Utility.LinkedWorldObject; using QSB.WorldSync; +using System; namespace QSB.EchoesOfTheEye.RaftSync.TransformSync; @@ -11,10 +11,17 @@ public class RaftTransformSync : UnsectoredRigidbodySync, ILinkedNetworkBehaviou { protected override bool UseInterpolation => false; - private QSBRaft _qsbRaft; - public void SetWorldObject(IWorldObject worldObject) => _qsbRaft = (QSBRaft)worldObject; + private IWorldObject _worldObject; + public void SetWorldObject(IWorldObject worldObject) => _worldObject = worldObject; - protected override OWRigidbody InitAttachedRigidbody() => _qsbRaft.AttachedObject._raftBody; + protected override OWRigidbody InitAttachedRigidbody() => + _worldObject.AttachedObject switch + { + RaftController x => x._raftBody, + DreamRaftController x => x._raftBody, + SealRaftController x => x._raftBody, + _ => throw new ArgumentOutOfRangeException(nameof(_worldObject.AttachedObject), _worldObject.AttachedObject, null) + }; public override void OnStartClient() { diff --git a/QSB/Player/PlayerInfoParts/Conversation.cs b/QSB/Player/PlayerInfoParts/Conversation.cs index 7712e9e8..4c6ce0cd 100644 --- a/QSB/Player/PlayerInfoParts/Conversation.cs +++ b/QSB/Player/PlayerInfoParts/Conversation.cs @@ -1,9 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using UnityEngine; +using UnityEngine; namespace QSB.Player; diff --git a/QSB/Player/PlayerInfoParts/DreamWorld.cs b/QSB/Player/PlayerInfoParts/DreamWorld.cs index 6bf47863..4060de67 100644 --- a/QSB/Player/PlayerInfoParts/DreamWorld.cs +++ b/QSB/Player/PlayerInfoParts/DreamWorld.cs @@ -1,9 +1,4 @@ using QSB.ItemSync.WorldObjects.Items; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace QSB.Player; diff --git a/QSB/Player/PlayerInfoParts/LocalTools.cs b/QSB/Player/PlayerInfoParts/LocalTools.cs index 8d13124e..7af6d84e 100644 --- a/QSB/Player/PlayerInfoParts/LocalTools.cs +++ b/QSB/Player/PlayerInfoParts/LocalTools.cs @@ -11,7 +11,7 @@ public partial class PlayerInfo { if (!IsLocalPlayer) { - DebugLog.ToConsole($"Warning - Tried to access local-only property LocalProbeLauncher in PlayerInfo for non local player!", MessageType.Warning); + DebugLog.ToConsole("Warning - Tried to access local-only property LocalProbeLauncher in PlayerInfo for non local player!", MessageType.Warning); return null; } @@ -25,7 +25,7 @@ public partial class PlayerInfo { if (!IsLocalPlayer) { - DebugLog.ToConsole($"Warning - Tried to access local-only property LocalFlashlight in PlayerInfo for non local player!", MessageType.Warning); + DebugLog.ToConsole("Warning - Tried to access local-only property LocalFlashlight in PlayerInfo for non local player!", MessageType.Warning); return null; } @@ -39,7 +39,7 @@ public partial class PlayerInfo { if (!IsLocalPlayer) { - DebugLog.ToConsole($"Warning - Tried to access local-only property LocalSignalscope in PlayerInfo for non local player!", MessageType.Warning); + DebugLog.ToConsole("Warning - Tried to access local-only property LocalSignalscope in PlayerInfo for non local player!", MessageType.Warning); return null; } @@ -53,7 +53,7 @@ public partial class PlayerInfo { if (!IsLocalPlayer) { - DebugLog.ToConsole($"Warning - Tried to access local-only property LocalTranslator in PlayerInfo for non local player!", MessageType.Warning); + DebugLog.ToConsole("Warning - Tried to access local-only property LocalTranslator in PlayerInfo for non local player!", MessageType.Warning); return null; } diff --git a/QSB/ShipSync/ShipCustomAttach.cs b/QSB/ShipSync/ShipCustomAttach.cs index 84f16cdc..d113260a 100644 --- a/QSB/ShipSync/ShipCustomAttach.cs +++ b/QSB/ShipSync/ShipCustomAttach.cs @@ -11,8 +11,8 @@ public class ShipCustomAttach : MonoBehaviour private void Awake() { - Locator.GetPromptManager().AddScreenPrompt(_attachPrompt, PromptPosition.UpperRight); - Locator.GetPromptManager().AddScreenPrompt(_detachPrompt, PromptPosition.UpperRight); + Locator.GetPromptManager().AddScreenPrompt(_attachPrompt, PromptPosition.Center); + Locator.GetPromptManager().AddScreenPrompt(_detachPrompt, PromptPosition.Center); _playerAttachPoint = gameObject.AddComponent(); _playerAttachPoint._lockPlayerTurning = false; @@ -24,8 +24,8 @@ public class ShipCustomAttach : MonoBehaviour { if (Locator.GetPromptManager()) { - Locator.GetPromptManager().RemoveScreenPrompt(_attachPrompt, PromptPosition.UpperRight); - Locator.GetPromptManager().RemoveScreenPrompt(_detachPrompt, PromptPosition.UpperRight); + Locator.GetPromptManager().RemoveScreenPrompt(_attachPrompt, PromptPosition.Center); + Locator.GetPromptManager().RemoveScreenPrompt(_detachPrompt, PromptPosition.Center); } } diff --git a/QSB/Utility/DebugActions.cs b/QSB/Utility/DebugActions.cs index 7eb07136..a656e1c6 100644 --- a/QSB/Utility/DebugActions.cs +++ b/QSB/Utility/DebugActions.cs @@ -1,8 +1,10 @@ -using QSB.Messaging; +using QSB.ItemSync.WorldObjects.Items; +using QSB.Messaging; using QSB.Player; using QSB.RespawnSync; using QSB.ShipSync; using QSB.Utility.Messages; +using QSB.WorldSync; using System.Linq; using UnityEngine; using UnityEngine.InputSystem; @@ -45,7 +47,7 @@ public class DebugActions : MonoBehaviour, IAddComponentOnStart /* * 1 - Warp to first non local player - * 2 - Set time flowing + * 2 - Enter dream world * 3 - Destroy probe * 4 - Damage ship electricals * 5 - Trigger supernova @@ -66,7 +68,32 @@ public class DebugActions : MonoBehaviour, IAddComponentOnStart if (Keyboard.current[Key.Numpad2].wasPressedThisFrame) { - TimeLoop._isTimeFlowing = true; + if (!QSBPlayerManager.LocalPlayer.InDreamWorld) + { + var relativeLocation = new RelativeLocationData(Vector3.up * 2 + Vector3.forward * 2, Quaternion.identity, Vector3.zero); + + const DreamArrivalPoint.Location location = DreamArrivalPoint.Location.Zone3; + var arrivalPoint = Locator.GetDreamArrivalPoint(location); + var dreamCampfire = Locator.GetDreamCampfire(location); + if (Locator.GetToolModeSwapper().GetItemCarryTool().GetHeldItemType() != ItemType.DreamLantern) + { + var dreamLanternItem = QSBWorldSync.GetWorldObjects().First(x => + x.AttachedObject._lanternType == DreamLanternType.Functioning && + QSBPlayerManager.PlayerList.All(y => y.HeldItem != x) + ).AttachedObject; + Locator.GetToolModeSwapper().GetItemCarryTool().PickUpItemInstantly(dreamLanternItem); + } + + Locator.GetDreamWorldController().EnterDreamWorld(dreamCampfire, arrivalPoint, relativeLocation); + } + else + { + if (Locator.GetToolModeSwapper().GetItemCarryTool().GetHeldItemType() != ItemType.DreamLantern) + { + var dreamLanternItem = QSBPlayerManager.LocalPlayer.AssignedSimulationLantern.AttachedObject; + Locator.GetToolModeSwapper().GetItemCarryTool().PickUpItemInstantly(dreamLanternItem); + } + } } if (Keyboard.current[Key.Numpad3].wasPressedThisFrame) @@ -122,4 +149,4 @@ public class DebugActions : MonoBehaviour, IAddComponentOnStart RespawnManager.Instance.RespawnSomePlayer(); } } -} \ No newline at end of file +} diff --git a/README.md b/README.md index 062ea6bb..873873e1 100644 --- a/README.md +++ b/README.md @@ -93,7 +93,7 @@ Long answer : Pay me enough money, and maybe I'll consider it. Boring boring physics stuff. The velocity of the ship is synced, as well as the angular velocity. However, this velocity is not also applied to the player. (Or it is sometimes. I don't 100% know.) This means the ship will accelerate, leaving the player "behind". Which makes you fly into the walls alot. -**Update**: you can attach/detach yourself to/from the ship using the prompt in the upper right corner of the screen. +**Update**: you can attach/detach yourself to/from the ship using the prompt in the center of the screen. ### What's the difference between QSB and Outer Wilds Online?