diff --git a/EpicOnlineTransport/EpicOnlineTransport.csproj b/EpicOnlineTransport/EpicOnlineTransport.csproj index 1134fb47..5655714a 100644 --- a/EpicOnlineTransport/EpicOnlineTransport.csproj +++ b/EpicOnlineTransport/EpicOnlineTransport.csproj @@ -4,6 +4,6 @@ - + diff --git a/EpicRerouter/EpicRerouter.csproj b/EpicRerouter/EpicRerouter.csproj index a582e089..dbebee21 100644 --- a/EpicRerouter/EpicRerouter.csproj +++ b/EpicRerouter/EpicRerouter.csproj @@ -3,7 +3,7 @@ Exe - + diff --git a/MirrorWeaver/MirrorWeaver.csproj b/MirrorWeaver/MirrorWeaver.csproj index d74b3074..737bc8f8 100644 --- a/MirrorWeaver/MirrorWeaver.csproj +++ b/MirrorWeaver/MirrorWeaver.csproj @@ -3,7 +3,7 @@ Exe - + diff --git a/QSB.sln.DotSettings b/QSB.sln.DotSettings index 2338a938..5a30abd4 100644 --- a/QSB.sln.DotSettings +++ b/QSB.sln.DotSettings @@ -7,16 +7,7 @@ Required Required Required - FileScoped - False - False - USE_TABS_ONLY NEXT_LINE_SHIFTED_2 - Tab - 1 - 1 - True - False True True True diff --git a/QSB/Animation/NPC/CharacterAnimManager.cs b/QSB/Animation/NPC/CharacterAnimManager.cs index 5e223dda..78a034d6 100644 --- a/QSB/Animation/NPC/CharacterAnimManager.cs +++ b/QSB/Animation/NPC/CharacterAnimManager.cs @@ -9,9 +9,6 @@ internal class CharacterAnimManager : WorldObjectManager { public override WorldObjectScene WorldObjectScene => WorldObjectScene.Both; - public override async UniTask BuildWorldObjects(OWScene scene, CancellationToken ct) - { - QSBWorldSync.Init(); + public override async UniTask BuildWorldObjects(OWScene scene, CancellationToken ct) => QSBWorldSync.Init(); - } } \ No newline at end of file diff --git a/QSB/Animation/NPC/Patches/CharacterAnimationPatches.cs b/QSB/Animation/NPC/Patches/CharacterAnimationPatches.cs index 2d15a198..cc93e9c6 100644 --- a/QSB/Animation/NPC/Patches/CharacterAnimationPatches.cs +++ b/QSB/Animation/NPC/Patches/CharacterAnimationPatches.cs @@ -115,23 +115,4 @@ public class CharacterAnimationPatches : QSBPatch return false; } - - [HarmonyPrefix] - [HarmonyPatch(typeof(KidRockController), nameof(KidRockController.Update))] - public static bool UpdateReplacement(KidRockController __instance) - { - if (!QSBWorldSync.AllObjectsReady) - { - return true; - } - - var qsbObj = QSBWorldSync.GetWorldObjects().First(x => x.GetDialogueTree() == __instance._dialogueTree); - - if (!__instance._throwingRock && !qsbObj.InConversation() && Time.time > __instance._nextThrowTime) - { - __instance.StartRockThrow(); - } - - return false; - } } \ No newline at end of file diff --git a/QSB/Animation/NPC/WorldObjects/QSBCharacterAnimController.cs b/QSB/Animation/NPC/WorldObjects/QSBCharacterAnimController.cs deleted file mode 100644 index b372d94b..00000000 --- a/QSB/Animation/NPC/WorldObjects/QSBCharacterAnimController.cs +++ /dev/null @@ -1,17 +0,0 @@ -using QSB.WorldSync; - -namespace QSB.Animation.NPC.WorldObjects; - -internal class QSBCharacterAnimController : WorldObject -{ - public override void SendInitialState(uint to) - { - // todo SendInitialState - } - - public CharacterDialogueTree GetDialogueTree() - => AttachedObject._dialogueTree; - - public bool InConversation() - => AttachedObject._inConversation; -} \ No newline at end of file diff --git a/QSB/Animation/NPC/WorldObjects/QSBSolanumAnimController.cs b/QSB/Animation/NPC/WorldObjects/QSBSolanumAnimController.cs index ff55aaf8..776decd2 100644 --- a/QSB/Animation/NPC/WorldObjects/QSBSolanumAnimController.cs +++ b/QSB/Animation/NPC/WorldObjects/QSBSolanumAnimController.cs @@ -11,6 +11,4 @@ internal class QSBSolanumAnimController : WorldObject { private QSBSolanumTrigger _trigger; public QSBSolanumTrigger Trigger => _trigger ??= QSBWorldSync.GetWorldObjects().First(); - - public override void SendInitialState(uint to) { } } \ No newline at end of file diff --git a/QSB/Animation/Player/AnimationSync.cs b/QSB/Animation/Player/AnimationSync.cs index 2b0069f4..e7e1d18a 100644 --- a/QSB/Animation/Player/AnimationSync.cs +++ b/QSB/Animation/Player/AnimationSync.cs @@ -6,6 +6,7 @@ using QSB.Animation.Player.Thrusters; using QSB.Messaging; using QSB.Player; using QSB.Utility; +using QSB.WorldSync; using System; using UnityEngine; @@ -17,8 +18,6 @@ public class AnimationSync : PlayerSyncObject private AnimatorOverrideController _unsuitedAnimController; private GameObject _suitedGraphics; private GameObject _unsuitedGraphics; - private PlayerCharacterController _playerController; - private CrouchSync _crouchSync; public AnimatorMirror Mirror { get; private set; } public bool InSuitedUpState { get; set; } @@ -31,16 +30,26 @@ public class AnimationSync : PlayerSyncObject InvisibleAnimator = gameObject.GetRequiredComponent(); NetworkAnimator = gameObject.GetRequiredComponent(); NetworkAnimator.enabled = false; + RequestInitialStatesMessage.SendInitialState += SendInitialState; } + protected void OnDestroy() => RequestInitialStatesMessage.SendInitialState -= SendInitialState; + + /// + /// This wipes the NetworkAnimator's fields, so it assumes the parameters have changed. + /// Basically just forces it to set all its dirty flags. + /// + private void SendInitialState(uint to) => NetworkAnimator.Invoke("Awake"); + + public void Reset() => InSuitedUpState = false; + private void InitCommon(Transform modelRoot) { try { - if (modelRoot == null) { - DebugLog.ToConsole($"Error - Trying to InitCommon with null body!", MessageType.Error); + DebugLog.ToConsole("Error - Trying to InitCommon with null body!", MessageType.Error); return; } @@ -48,11 +57,11 @@ public class AnimationSync : PlayerSyncObject Mirror = modelRoot.gameObject.AddComponent(); if (isLocalPlayer) { - Mirror.Init(VisibleAnimator, InvisibleAnimator); + Mirror.Init(VisibleAnimator, InvisibleAnimator, NetworkAnimator); } else { - Mirror.Init(InvisibleAnimator, VisibleAnimator); + Mirror.Init(InvisibleAnimator, VisibleAnimator, null); } NetworkAnimator.enabled = true; @@ -74,10 +83,6 @@ public class AnimationSync : PlayerSyncObject public void InitLocal(Transform body) { InitCommon(body); - - _playerController = body.parent.GetComponent(); - - InitCrouchSync(); InitAccelerationSync(); } @@ -85,7 +90,6 @@ public class AnimationSync : PlayerSyncObject { InitCommon(body); SetSuitState(QSBSceneManager.CurrentScene == OWScene.EyeOfTheUniverse); - InitCrouchSync(); InitAccelerationSync(); ThrusterManager.CreateRemotePlayerVFX(Player); @@ -100,12 +104,6 @@ public class AnimationSync : PlayerSyncObject Player.JetpackAcceleration.Init(thrusterModel); } - private void InitCrouchSync() - { - _crouchSync = this.GetRequiredComponent(); - _crouchSync.Init(_playerController, VisibleAnimator); - } - public void SetSuitState(bool suitedUp) { if (!Player.IsReady) @@ -176,12 +174,12 @@ public class AnimationSync : PlayerSyncObject // Avoids "jumping" when putting on suit if (VisibleAnimator != null) { - VisibleAnimator.SetTrigger("Grounded"); + VisibleAnimator.SetBool("Grounded", true); } if (InvisibleAnimator != null) { - InvisibleAnimator.SetTrigger("Grounded"); + InvisibleAnimator.SetBool("Grounded", true); } if (NetworkAnimator == null) @@ -196,4 +194,4 @@ public class AnimationSync : PlayerSyncObject Mirror.RebuildFloatParams(); NetworkAnimator.Invoke("Awake"); } -} \ No newline at end of file +} diff --git a/QSB/Animation/Player/AnimatorMirror.cs b/QSB/Animation/Player/AnimatorMirror.cs index bbc1c525..736d5b2e 100644 --- a/QSB/Animation/Player/AnimatorMirror.cs +++ b/QSB/Animation/Player/AnimatorMirror.cs @@ -1,4 +1,5 @@ -using OWML.Common; +using Mirror; +using OWML.Common; using QSB.Utility; using System.Collections.Generic; using System.Linq; @@ -12,10 +13,17 @@ public class AnimatorMirror : MonoBehaviour private Animator _from; private Animator _to; + private NetworkAnimator _networkAnimator; private readonly Dictionary _floatParams = new(); - public void Init(Animator from, Animator to) + /// + /// Initializes the Animator Mirror + /// + /// The Animator to take the values from. + /// The Animator to set the values on to. + /// The NetworkAnimator to sync triggers through. Set only if you have auth over "", otherwise set to null. + public void Init(Animator from, Animator to, NetworkAnimator netAnimator) { if (from == null) { @@ -44,6 +52,8 @@ public class AnimatorMirror : MonoBehaviour _to.runtimeAnimatorController = _from.runtimeAnimatorController; } + _networkAnimator = netAnimator; + RebuildFloatParams(); } @@ -61,6 +71,7 @@ public class AnimatorMirror : MonoBehaviour } SyncParams(); + SyncLayerWeights(); SmoothFloats(); } @@ -74,13 +85,49 @@ public class AnimatorMirror : MonoBehaviour _floatParams[fromParam.name].Target = _from.GetFloat(fromParam.name); break; + case AnimatorControllerParameterType.Int: + _to.SetInteger(fromParam.name, _from.GetInteger(fromParam.name)); + break; + case AnimatorControllerParameterType.Bool: _to.SetBool(fromParam.name, _from.GetBool(fromParam.name)); break; + + case AnimatorControllerParameterType.Trigger: + if (_from.GetBool(fromParam.name) && !_to.GetBool(fromParam.name)) + { + if (_networkAnimator != null) + { + _networkAnimator.SetTrigger(fromParam.name); + } + + _to.SetTrigger(fromParam.name); + } + + if (!_from.GetBool(fromParam.name) && _to.GetBool(fromParam.name)) + { + if (_networkAnimator != null) + { + _networkAnimator.ResetTrigger(fromParam.name); + } + + _to.ResetTrigger(fromParam.name); + } + + break; } } } + private void SyncLayerWeights() + { + for (var i = 0; i < _from.layerCount; i++) + { + var weight = _from.GetLayerWeight(i); + _to.SetLayerWeight(i, weight); + } + } + private void SmoothFloats() { foreach (var floatParam in _floatParams) @@ -98,4 +145,4 @@ public class AnimatorMirror : MonoBehaviour _floatParams.Add(param.name, new AnimFloatParam()); } } -} \ No newline at end of file +} diff --git a/QSB/Animation/Player/CrouchSync.cs b/QSB/Animation/Player/CrouchSync.cs deleted file mode 100644 index 33beebc6..00000000 --- a/QSB/Animation/Player/CrouchSync.cs +++ /dev/null @@ -1,59 +0,0 @@ -using Mirror; -using QSB.Utility.VariableSync; -using UnityEngine; - -namespace QSB.Animation.Player; - -public class CrouchSync : NetworkBehaviour -{ - public AnimFloatParam CrouchParam { get; } = new AnimFloatParam(); - - private const float CrouchSmoothTime = 0.05f; - public const int CrouchLayerIndex = 1; - - private PlayerCharacterController _playerController; - private Animator _bodyAnim; - - public FloatVariableSyncer CrouchVariableSyncer; - - public void Init(PlayerCharacterController playerController, Animator bodyAnim) - { - _playerController = playerController; - _bodyAnim = bodyAnim; - } - - public void Update() - { - if (isLocalPlayer) - { - SyncLocalCrouch(); - return; - } - - SyncRemoteCrouch(); - } - - private void SyncLocalCrouch() - { - if (_playerController == null) - { - return; - } - - var jumpChargeFraction = _playerController.GetJumpCrouchFraction(); - CrouchVariableSyncer.Value = jumpChargeFraction; - } - - private void SyncRemoteCrouch() - { - if (_bodyAnim == null) - { - return; - } - - CrouchParam.Target = CrouchVariableSyncer.Value; - CrouchParam.Smooth(CrouchSmoothTime); - var jumpChargeFraction = CrouchParam.Current; - _bodyAnim.SetLayerWeight(CrouchLayerIndex, jumpChargeFraction); - } -} \ No newline at end of file diff --git a/QSB/Animation/Player/Messages/AnimationTriggerMessage.cs b/QSB/Animation/Player/Messages/AnimationTriggerMessage.cs deleted file mode 100644 index c7166e4c..00000000 --- a/QSB/Animation/Player/Messages/AnimationTriggerMessage.cs +++ /dev/null @@ -1,28 +0,0 @@ -using QSB.Messaging; -using QSB.Player; -using QSB.WorldSync; - -namespace QSB.Animation.Player.Messages; - -internal class AnimationTriggerMessage : QSBMessage -{ - public AnimationTriggerMessage(string name) : base(name) { } - - public override bool ShouldReceive => QSBWorldSync.AllObjectsReady; - - public override void OnReceiveRemote() - { - var animationSync = QSBPlayerManager.GetPlayer(From).AnimationSync; - if (animationSync == null) - { - return; - } - - if (animationSync.VisibleAnimator == null) - { - return; - } - - animationSync.VisibleAnimator.SetTrigger(Data); - } -} \ No newline at end of file diff --git a/QSB/Animation/Player/Patches/PlayerAnimationPatches.cs b/QSB/Animation/Player/Patches/PlayerAnimationPatches.cs deleted file mode 100644 index e99b52e7..00000000 --- a/QSB/Animation/Player/Patches/PlayerAnimationPatches.cs +++ /dev/null @@ -1,132 +0,0 @@ -using HarmonyLib; -using QSB.Animation.Player.Messages; -using QSB.Messaging; -using QSB.Patches; -using QSB.Utility; -using UnityEngine; - -namespace QSB.Animation.Player.Patches; - -[HarmonyPatch] -internal class PlayerAnimationPatches : QSBPatch -{ - public override QSBPatchTypes Type => QSBPatchTypes.OnClientConnect; - - [HarmonyPrefix] - [HarmonyPatch(typeof(PlayerAnimController), nameof(PlayerAnimController.LateUpdate))] - public static bool LateUpdateReplacement( - PlayerAnimController __instance) - { - var isGrounded = __instance._playerController.IsGrounded(); - var isAttached = PlayerState.IsAttached(); - var isInZeroG = PlayerState.InZeroG(); - var isFlying = __instance._playerJetpack.GetLocalAcceleration().y > 0f; - var movementVector = Vector3.zero; - if (!isAttached) - { - movementVector = __instance._playerController.GetRelativeGroundVelocity(); - } - - if (Mathf.Abs(movementVector.x) < 0.05f) - { - movementVector.x = 0f; - } - - if (Mathf.Abs(movementVector.z) < 0.05f) - { - movementVector.z = 0f; - } - - if (isFlying) - { - __instance._ungroundedTime = Time.time; - } - - var freefallMagnitude = 0f; - var timeInFreefall = 0f; - var lastGroundBody = __instance._playerController.GetLastGroundBody(); - if (!isGrounded && !isAttached && !isInZeroG && lastGroundBody != null) - { - freefallMagnitude = (__instance._playerController.GetAttachedOWRigidbody().GetVelocity() - lastGroundBody.GetPointVelocity(__instance._playerController.transform.position)).magnitude; - timeInFreefall = Time.time - __instance._ungroundedTime; - } - - __instance._animator.SetFloat("RunSpeedX", movementVector.x / 3f); - __instance._animator.SetFloat("RunSpeedY", movementVector.z / 3f); - __instance._animator.SetFloat("TurnSpeed", __instance._playerController.GetTurning()); - __instance._animator.SetBool("Grounded", isGrounded || isAttached || PlayerState.IsRecentlyDetached()); - __instance._animator.SetLayerWeight(CrouchSync.CrouchLayerIndex, __instance._playerController.GetJumpCrouchFraction()); - __instance._animator.SetFloat("FreefallSpeed", freefallMagnitude / 15f * (timeInFreefall / 3f)); - __instance._animator.SetBool("InZeroG", isInZeroG || isFlying); - __instance._animator.SetBool("UsingJetpack", isInZeroG && PlayerState.IsWearingSuit()); - if (__instance._justBecameGrounded) - { - if (__instance._justTookFallDamage) - { - __instance._animator.SetTrigger("LandHard"); - new AnimationTriggerMessage("LandHard").Send(); - } - else - { - __instance._animator.SetTrigger("Land"); - new AnimationTriggerMessage("Land").Send(); - } - } - - if (isGrounded) - { - var leftFootLift = __instance._animator.GetFloat("LeftFootLift"); - if (!__instance._leftFootGrounded && leftFootLift < 0.333f) - { - __instance._leftFootGrounded = true; - __instance.RaiseEvent(nameof(__instance.OnLeftFootGrounded)); - } - else if (__instance._leftFootGrounded && leftFootLift > 0.666f) - { - __instance._leftFootGrounded = false; - __instance.RaiseEvent(nameof(__instance.OnLeftFootLift)); - } - - var rightFootLift = __instance._animator.GetFloat("RightFootLift"); - if (!__instance._rightFootGrounded && rightFootLift < 0.333f) - { - __instance._rightFootGrounded = true; - __instance.RaiseEvent(nameof(__instance.OnRightFootGrounded)); - } - else if (__instance._rightFootGrounded && rightFootLift > 0.666f) - { - __instance._rightFootGrounded = false; - __instance.RaiseEvent(nameof(__instance.OnRightFootLift)); - } - } - - __instance._justBecameGrounded = false; - __instance._justTookFallDamage = false; - var usingTool = Locator.GetToolModeSwapper().GetToolMode() != ToolMode.None; - if ((usingTool && !__instance._rightArmHidden) || (!usingTool && __instance._rightArmHidden)) - { - __instance._rightArmHidden = usingTool; - for (var i = 0; i < __instance._rightArmObjects.Length; i++) - { - __instance._rightArmObjects[i].layer = (!__instance._rightArmHidden) ? __instance._defaultLayer : __instance._probeOnlyLayer; - } - } - - return false; - } - - [HarmonyPrefix] - [HarmonyPatch(typeof(PlayerAnimController), nameof(PlayerAnimController.OnPlayerJump))] - public static bool OnPlayerJumpReplacement(PlayerAnimController __instance) - { - __instance._ungroundedTime = Time.time; - if (!__instance.isActiveAndEnabled) - { - return false; - } - - __instance._animator.SetTrigger("Jump"); - new AnimationTriggerMessage("Jump").Send(); - return false; - } -} \ No newline at end of file diff --git a/QSB/Animation/Player/PlayerHeadRotationSync.cs b/QSB/Animation/Player/PlayerHeadRotationSync.cs index d71b0f35..cc41f8d5 100644 --- a/QSB/Animation/Player/PlayerHeadRotationSync.cs +++ b/QSB/Animation/Player/PlayerHeadRotationSync.cs @@ -39,6 +39,7 @@ public class PlayerHeadRotationSync : MonoBehaviour var bone = _attachedAnimator.GetBoneTransform(HumanBodyBones.Head); // Get the camera's local rotation with respect to the player body var lookLocalRotation = Quaternion.Inverse(_attachedAnimator.transform.rotation) * _lookBase.rotation; + // no idea why this rotation is like this, but this is the only way it looks right bone.localRotation = Quaternion.Euler(-lookLocalRotation.eulerAngles.y, -lookLocalRotation.eulerAngles.z, lookLocalRotation.eulerAngles.x); } } \ No newline at end of file diff --git a/QSB/AssetBundles/qsb_network b/QSB/AssetBundles/qsb_network index 3332e0fc..5af36f0d 100644 Binary files a/QSB/AssetBundles/qsb_network and b/QSB/AssetBundles/qsb_network differ diff --git a/QSB/AssetBundles/qsb_network_big b/QSB/AssetBundles/qsb_network_big index 22247a5b..4688aa52 100644 Binary files a/QSB/AssetBundles/qsb_network_big and b/QSB/AssetBundles/qsb_network_big differ diff --git a/QSB/AuthoritySync/AuthWorldObject.cs b/QSB/AuthoritySync/AuthWorldObject.cs new file mode 100644 index 00000000..f70d64c7 --- /dev/null +++ b/QSB/AuthoritySync/AuthWorldObject.cs @@ -0,0 +1,39 @@ +using Cysharp.Threading.Tasks; +using QSB.Messaging; +using QSB.Player; +using QSB.WorldSync; +using System.Threading; +using UnityEngine; + +namespace QSB.AuthoritySync; + +/// +/// helper implementation of the interface +/// +public abstract class AuthWorldObject : WorldObject, IAuthWorldObject + where T : MonoBehaviour +{ + public uint Owner { get; set; } + public abstract bool CanOwn { get; } + + public override void SendInitialState(uint to) => + ((IAuthWorldObject)this).SendMessage(new AuthWorldObjectMessage(Owner) { To = to }); + + public override async UniTask Init(CancellationToken ct) => + QSBPlayerManager.OnRemovePlayer += OnPlayerLeave; + + public override void OnRemoval() => + QSBPlayerManager.OnRemovePlayer -= OnPlayerLeave; + + private void OnPlayerLeave(PlayerInfo player) + { + if (!QSBCore.IsHost) + { + return; + } + if (Owner == player.PlayerId) + { + ((IAuthWorldObject)this).SendMessage(new AuthWorldObjectMessage(CanOwn ? QSBPlayerManager.LocalPlayerId : 0)); + } + } +} diff --git a/QSB/AuthoritySync/AuthWorldObjectMessage.cs b/QSB/AuthoritySync/AuthWorldObjectMessage.cs new file mode 100644 index 00000000..08f9cb95 --- /dev/null +++ b/QSB/AuthoritySync/AuthWorldObjectMessage.cs @@ -0,0 +1,45 @@ +using QSB.Messaging; +using QSB.Player; + +namespace QSB.AuthoritySync; + +/// +/// request or release ownership of a world object +/// +public class AuthWorldObjectMessage : QSBWorldObjectMessage +{ + public AuthWorldObjectMessage(uint owner) : base(owner) { } + + public override bool ShouldReceive + { + get + { + if (!base.ShouldReceive) + { + return false; + } + + // Deciding if to change the object's owner + // Message + // | = 0 | > 0 | + // = 0 | No | Yes | + // > 0 | Yes | No | + // if Obj==Message then No + // Obj + + return (WorldObject.Owner == 0 || Data == 0) && WorldObject.Owner != Data; + } + } + + public override void OnReceiveLocal() => WorldObject.Owner = Data; + + public override void OnReceiveRemote() + { + WorldObject.Owner = Data; + if (WorldObject.Owner == 0 && WorldObject.CanOwn) + { + // object has no owner, but is still active for this player. request ownership + WorldObject.SendMessage(new AuthWorldObjectMessage(QSBPlayerManager.LocalPlayerId)); + } + } +} diff --git a/QSB/AuthoritySync/IAuthWorldObject.cs b/QSB/AuthoritySync/IAuthWorldObject.cs new file mode 100644 index 00000000..7d706feb --- /dev/null +++ b/QSB/AuthoritySync/IAuthWorldObject.cs @@ -0,0 +1,18 @@ +using QSB.WorldSync; + +namespace QSB.AuthoritySync; + +/// +/// a world object that has an owner +/// +public interface IAuthWorldObject : IWorldObject +{ + /// + /// 0 = owned by no one + /// + public uint Owner { get; set; } + /// + /// can the world object have authority + /// + public bool CanOwn { get; } +} diff --git a/QSB/AuthoritySync/IAuthWorldObject_Extensions.cs b/QSB/AuthoritySync/IAuthWorldObject_Extensions.cs new file mode 100644 index 00000000..e0339625 --- /dev/null +++ b/QSB/AuthoritySync/IAuthWorldObject_Extensions.cs @@ -0,0 +1,32 @@ +using QSB.Messaging; +using QSB.Player; + +namespace QSB.AuthoritySync; + +public static class IAuthWorldObject_Extensions +{ + /// + /// try and gain authority over the object + /// + public static void RequestOwnership(this IAuthWorldObject @this) + { + if (@this.Owner != 0) + { + return; + } + @this.SendMessage(new AuthWorldObjectMessage(QSBPlayerManager.LocalPlayerId)); + } + + /// + /// release authority over the object, + /// potentially to giving it to someone else + /// + public static void ReleaseOwnership(this IAuthWorldObject @this) + { + if (@this.Owner != QSBPlayerManager.LocalPlayerId) + { + return; + } + @this.SendMessage(new AuthWorldObjectMessage(0)); + } +} diff --git a/QSB/CampfireSync/Messages/BurnSlideReelMessage.cs b/QSB/CampfireSync/Messages/BurnSlideReelMessage.cs index 0b2cd47d..4c4f5f7c 100644 --- a/QSB/CampfireSync/Messages/BurnSlideReelMessage.cs +++ b/QSB/CampfireSync/Messages/BurnSlideReelMessage.cs @@ -6,13 +6,16 @@ using QSB.WorldSync; namespace QSB.CampfireSync.Messages; +/// +/// TODO: initial state on campfire and item +/// internal class BurnSlideReelMessage : QSBWorldObjectMessage { public BurnSlideReelMessage(QSBCampfire campfire) : base(campfire.ObjectId) { } public override void OnReceiveRemote() { - var campfire = QSBWorldSync.GetWorldObject(Data).AttachedObject; + var campfire = Data.GetWorldObject().AttachedObject; var fromPlayer = QSBPlayerManager.GetPlayer(From); WorldObject.DropItem( campfire._burnedSlideReelSocket.position, diff --git a/QSB/ConversationSync/ConversationManager.cs b/QSB/ConversationSync/ConversationManager.cs index 2b1a78f7..f394e6f2 100644 --- a/QSB/ConversationSync/ConversationManager.cs +++ b/QSB/ConversationSync/ConversationManager.cs @@ -45,7 +45,7 @@ public class ConversationManager : WorldObjectManager QSBWorldSync.Init(); } - public uint GetPlayerTalkingToTree(CharacterDialogueTree tree) => + public uint GetPlayerTalkingToTree(CharacterDialogueTree tree) => QSBPlayerManager.PlayerList.FirstOrDefault(x => x.CurrentCharacterDialogueTree?.AttachedObject == tree) ?.PlayerId ?? uint.MaxValue; @@ -62,7 +62,7 @@ public class ConversationManager : WorldObjectManager new ConversationMessage(ConversationType.CloseCharacter, id).Send(); public void SendConvState(QSBCharacterDialogueTree tree, bool state) - => tree.SendMessage(new ConversationStartEndMessage(state)); + => tree.SendMessage(new ConversationStartEndMessage(QSBPlayerManager.LocalPlayerId, state)); public void DisplayPlayerConversationBox(uint playerId, string text) { @@ -109,4 +109,4 @@ public class ConversationManager : WorldObjectManager newBox.SetActive(true); return newBox; } -} \ No newline at end of file +} diff --git a/QSB/ConversationSync/Messages/ConversationStartEndMessage.cs b/QSB/ConversationSync/Messages/ConversationStartEndMessage.cs index ce88fc4b..1a1d8eb2 100644 --- a/QSB/ConversationSync/Messages/ConversationStartEndMessage.cs +++ b/QSB/ConversationSync/Messages/ConversationStartEndMessage.cs @@ -5,21 +5,21 @@ using QSB.Utility; namespace QSB.ConversationSync.Messages; -public class ConversationStartEndMessage : QSBWorldObjectMessage +public class ConversationStartEndMessage : QSBWorldObjectMessage { - public ConversationStartEndMessage(bool start) : base(start) { } + public ConversationStartEndMessage(uint playerId, bool start) : base((playerId, start)) { } public override void OnReceiveRemote() { - if (Data) + if (Data.start) { - QSBPlayerManager.GetPlayer(From).CurrentCharacterDialogueTree = WorldObject; + QSBPlayerManager.GetPlayer(Data.playerId).CurrentCharacterDialogueTree = WorldObject; WorldObject.AttachedObject.GetInteractVolume().DisableInteraction(); WorldObject.AttachedObject.RaiseEvent(nameof(CharacterDialogueTree.OnStartConversation)); } else { - QSBPlayerManager.GetPlayer(From).CurrentCharacterDialogueTree = null; + QSBPlayerManager.GetPlayer(Data.playerId).CurrentCharacterDialogueTree = null; WorldObject.AttachedObject.GetInteractVolume().EnableInteraction(); WorldObject.AttachedObject.RaiseEvent(nameof(CharacterDialogueTree.OnEndConversation)); } diff --git a/QSB/ConversationSync/WorldObjects/QSBCharacterDialogueTree.cs b/QSB/ConversationSync/WorldObjects/QSBCharacterDialogueTree.cs index f3eb5da8..17e8d329 100644 --- a/QSB/ConversationSync/WorldObjects/QSBCharacterDialogueTree.cs +++ b/QSB/ConversationSync/WorldObjects/QSBCharacterDialogueTree.cs @@ -1,9 +1,6 @@ -using QSB.WorldSync; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using QSB.ConversationSync.Messages; +using QSB.Messaging; +using QSB.WorldSync; namespace QSB.ConversationSync.WorldObjects; @@ -11,6 +8,11 @@ public class QSBCharacterDialogueTree : WorldObject { public override void SendInitialState(uint to) { - // todo : implement this + var playerId = ConversationManager.Instance.GetPlayerTalkingToTree(AttachedObject); + if (playerId != uint.MaxValue) + { + this.SendMessage(new ConversationStartEndMessage(playerId, true) { To = to }); + } + // TODO: maybe also sync the dialogue box and player box? } } diff --git a/QSB/DeathSync/RespawnOnDeath.cs b/QSB/DeathSync/RespawnOnDeath.cs index c990d24d..9968c1f9 100644 --- a/QSB/DeathSync/RespawnOnDeath.cs +++ b/QSB/DeathSync/RespawnOnDeath.cs @@ -1,7 +1,9 @@ using OWML.Common; +using QSB.Localization; using QSB.Player; using QSB.Player.TransformSync; using QSB.RespawnSync; +using QSB.ShipSync; using QSB.Utility; using QSB.WorldSync; using System.Linq; @@ -26,6 +28,7 @@ public class RespawnOnDeath : MonoBehaviour private PlayerSpacesuit _spaceSuit; private SuitPickupVolume[] _suitPickupVolumes; private Vector3 _deathPositionRelative; + private GUIStyle _deadTextStyle; public Transform DeathClosestAstroObject { get; private set; } public Vector3 DeathPositionWorld @@ -47,6 +50,11 @@ public class RespawnOnDeath : MonoBehaviour _suitPickupVolumes = FindObjectsOfType(); _fluidDetector = Locator.GetPlayerCamera().GetComponentInChildren(); _playerSpawnPoint = GetSpawnPoint(); + _deadTextStyle = new(); + _deadTextStyle.font = (Font)Resources.Load(@"fonts\english - latin\SpaceMono-Regular_Dynamic"); + _deadTextStyle.alignment = TextAnchor.MiddleCenter; + _deadTextStyle.normal.textColor = Color.white; + _deadTextStyle.fontSize = 20; } public void ResetPlayer() @@ -122,4 +130,31 @@ public class RespawnOnDeath : MonoBehaviour spawnPoint.GetSpawnLocation() == SpawnLocation.TimberHearth && spawnPoint.IsShipSpawn() == false); } + + void OnGUI() + { + if (PlayerTransformSync.LocalInstance == null || ShipManager.Instance.ShipCockpitUI == null) + { + return; + } + + if (QSBPlayerManager.LocalPlayer.IsDead) + { + GUI.contentColor = Color.white; + + var width = 200; + var height = 100; + + // it is good day to be not dead + + var secondText = ShipManager.Instance.IsShipWrecked + ? string.Format(QSBLocalization.Current.WaitingForAllToDie, QSBPlayerManager.PlayerList.Count(x => !x.IsDead)) + : QSBLocalization.Current.WaitingForRespawn; + + GUI.Label( + new Rect((Screen.width / 2) - (width / 2), (Screen.height / 2) - (height / 2) + (height * 2), width, height), + $"{QSBLocalization.Current.YouAreDead}\n{secondText}", + _deadTextStyle); + } + } } \ No newline at end of file diff --git a/QSB/EchoesOfTheEye/AirlockSync/AirlockManager.cs b/QSB/EchoesOfTheEye/AirlockSync/AirlockManager.cs index 5f2642b1..14a616e9 100644 --- a/QSB/EchoesOfTheEye/AirlockSync/AirlockManager.cs +++ b/QSB/EchoesOfTheEye/AirlockSync/AirlockManager.cs @@ -10,6 +10,9 @@ internal class AirlockManager : WorldObjectManager public override WorldObjectScene WorldObjectScene => WorldObjectScene.SolarSystem; public override bool DlcOnly => true; - public override async UniTask BuildWorldObjects(OWScene scene, CancellationToken ct) => + public override async UniTask BuildWorldObjects(OWScene scene, CancellationToken ct) + { QSBWorldSync.Init(); -} + QSBWorldSync.Init(); + } +} diff --git a/QSB/EchoesOfTheEye/AirlockSync/Messages/AirlockInitialStateMessage.cs b/QSB/EchoesOfTheEye/AirlockSync/Messages/AirlockInitialStateMessage.cs new file mode 100644 index 00000000..665f6421 --- /dev/null +++ b/QSB/EchoesOfTheEye/AirlockSync/Messages/AirlockInitialStateMessage.cs @@ -0,0 +1,17 @@ +using QSB.EchoesOfTheEye.AirlockSync.WorldObjects; +using QSB.Messaging; + +namespace QSB.EchoesOfTheEye.AirlockSync.Messages; + +internal class AirlockInitialStateMessage : QSBWorldObjectMessage +{ + public AirlockInitialStateMessage(bool innerDoorOpen, bool outerDoorOpen, bool pressurized) : base((innerDoorOpen, outerDoorOpen, pressurized)) { } + + public override void OnReceiveRemote() + { + var airlock = WorldObject.AttachedObject; + airlock._innerDoor.SetOpenImmediate(Data.innerDoorOpen); + airlock._outerDoor.SetOpenImmediate(Data.outerDoorOpen); + airlock.SetPressurization(Data.pressurized); + } +} diff --git a/QSB/EchoesOfTheEye/AirlockSync/WorldObjects/QSBAirlockInterface.cs b/QSB/EchoesOfTheEye/AirlockSync/WorldObjects/QSBAirlockInterface.cs index ed84ea35..93350633 100644 --- a/QSB/EchoesOfTheEye/AirlockSync/WorldObjects/QSBAirlockInterface.cs +++ b/QSB/EchoesOfTheEye/AirlockSync/WorldObjects/QSBAirlockInterface.cs @@ -1,4 +1,5 @@ using QSB.EchoesOfTheEye.AirlockSync.VariableSync; +using System; using System.Collections.Generic; using UnityEngine; @@ -9,4 +10,14 @@ internal class QSBAirlockInterface : QSBRotatingElements LightSensors => AttachedObject._lightSensors; protected override GameObject NetworkObjectPrefab => QSBNetworkManager.singleton.AirlockPrefab; + + public override string ReturnLabel() + { + var baseString = $"{this}{Environment.NewLine}CurrentRotation:{AttachedObject._currentRotation}"; + foreach (var element in AttachedObject._rotatingElements) + { + baseString += $"{Environment.NewLine}localRotation:{element.localRotation}"; + } + return baseString; + } } diff --git a/QSB/EchoesOfTheEye/AirlockSync/WorldObjects/QSBGhostAirlock.cs b/QSB/EchoesOfTheEye/AirlockSync/WorldObjects/QSBGhostAirlock.cs new file mode 100644 index 00000000..711db9b5 --- /dev/null +++ b/QSB/EchoesOfTheEye/AirlockSync/WorldObjects/QSBGhostAirlock.cs @@ -0,0 +1,17 @@ +using QSB.EchoesOfTheEye.AirlockSync.Messages; +using QSB.Messaging; +using QSB.WorldSync; + +namespace QSB.EchoesOfTheEye.AirlockSync.WorldObjects; + +internal class QSBGhostAirlock : WorldObject +{ + public override void SendInitialState(uint to) + => this.SendMessage( + new AirlockInitialStateMessage( + AttachedObject._innerDoor.IsOpen(), + AttachedObject._outerDoor.IsOpen(), + AttachedObject._pressurized + ) + ); +} diff --git a/QSB/EchoesOfTheEye/AlarmTotemSync/AlarmTotemManager.cs b/QSB/EchoesOfTheEye/AlarmTotemSync/AlarmTotemManager.cs index 4177a6c2..b50b1e90 100644 --- a/QSB/EchoesOfTheEye/AlarmTotemSync/AlarmTotemManager.cs +++ b/QSB/EchoesOfTheEye/AlarmTotemSync/AlarmTotemManager.cs @@ -15,7 +15,7 @@ public class AlarmTotemManager : WorldObjectManager public override async UniTask BuildWorldObjects(OWScene scene, CancellationToken ct) { - QSBWorldSync.Init(); + // QSBWorldSync.Init(); QSBWorldSync.Init(); _qsbAlarmSequenceController = new GameObject(nameof(QSBAlarmSequenceController)) diff --git a/QSB/EchoesOfTheEye/AlarmTotemSync/Patches/AlarmTotemPatches.cs b/QSB/EchoesOfTheEye/AlarmTotemSync/Patches/AlarmTotemPatches.cs index bebdf5f1..93169d62 100644 --- a/QSB/EchoesOfTheEye/AlarmTotemSync/Patches/AlarmTotemPatches.cs +++ b/QSB/EchoesOfTheEye/AlarmTotemSync/Patches/AlarmTotemPatches.cs @@ -12,6 +12,7 @@ public class AlarmTotemPatches : QSBPatch { public override QSBPatchTypes Type => QSBPatchTypes.OnClientConnect; + /* [HarmonyPrefix] [HarmonyPatch(typeof(AlarmTotem), nameof(AlarmTotem.OnSectorOccupantAdded))] private static void OnSectorOccupantAdded(AlarmTotem __instance, SectorDetector sectorDetector) @@ -80,6 +81,7 @@ public class AlarmTotemPatches : QSBPatch return false; } + */ [HarmonyPrefix] [HarmonyPatch(typeof(AlarmBell), nameof(AlarmBell.OnEntry))] diff --git a/QSB/EchoesOfTheEye/AlarmTotemSync/WorldObjects/QSBAlarmBell.cs b/QSB/EchoesOfTheEye/AlarmTotemSync/WorldObjects/QSBAlarmBell.cs index e5e1196f..0a58ff8d 100644 --- a/QSB/EchoesOfTheEye/AlarmTotemSync/WorldObjects/QSBAlarmBell.cs +++ b/QSB/EchoesOfTheEye/AlarmTotemSync/WorldObjects/QSBAlarmBell.cs @@ -2,7 +2,4 @@ namespace QSB.EchoesOfTheEye.AlarmTotemSync.WorldObjects; -public class QSBAlarmBell : WorldObject -{ - public override void SendInitialState(uint to) { } -} +public class QSBAlarmBell : WorldObject { } diff --git a/QSB/EchoesOfTheEye/AlarmTotemSync/WorldObjects/QSBAlarmTotem.cs b/QSB/EchoesOfTheEye/AlarmTotemSync/WorldObjects/QSBAlarmTotem.cs index e6801c82..69ff7f9b 100644 --- a/QSB/EchoesOfTheEye/AlarmTotemSync/WorldObjects/QSBAlarmTotem.cs +++ b/QSB/EchoesOfTheEye/AlarmTotemSync/WorldObjects/QSBAlarmTotem.cs @@ -8,6 +8,9 @@ using System.Threading; namespace QSB.EchoesOfTheEye.AlarmTotemSync.WorldObjects; +/// +/// TODO: make this not NRE (by not doing enable sync) and then readd it back in +/// public class QSBAlarmTotem : WorldObject { public readonly List VisibleFor = new(); diff --git a/QSB/EchoesOfTheEye/DreamLantern/WorldObjects/QSBDreamLantern.cs b/QSB/EchoesOfTheEye/DreamLantern/WorldObjects/QSBDreamLantern.cs index a3b4a90f..1f5fe10b 100644 --- a/QSB/EchoesOfTheEye/DreamLantern/WorldObjects/QSBDreamLantern.cs +++ b/QSB/EchoesOfTheEye/DreamLantern/WorldObjects/QSBDreamLantern.cs @@ -4,6 +4,9 @@ using QSB.WorldSync; namespace QSB.EchoesOfTheEye.DreamLantern.WorldObjects; +/// +/// TODO: lanterns held by ghosts should only be controlled by the host (to prevent it from visually freaking out) +/// public class QSBDreamLantern : WorldObject { public override void SendInitialState(uint to) diff --git a/QSB/EchoesOfTheEye/DreamRafts/WorldObjects/QSBDreamRaft.cs b/QSB/EchoesOfTheEye/DreamRafts/WorldObjects/QSBDreamRaft.cs index 56fdd832..37391ee6 100644 --- a/QSB/EchoesOfTheEye/DreamRafts/WorldObjects/QSBDreamRaft.cs +++ b/QSB/EchoesOfTheEye/DreamRafts/WorldObjects/QSBDreamRaft.cs @@ -6,8 +6,6 @@ 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/QSBSealRaft.cs b/QSB/EchoesOfTheEye/DreamRafts/WorldObjects/QSBSealRaft.cs index 6f5e5f59..f4385f4e 100644 --- a/QSB/EchoesOfTheEye/DreamRafts/WorldObjects/QSBSealRaft.cs +++ b/QSB/EchoesOfTheEye/DreamRafts/WorldObjects/QSBSealRaft.cs @@ -6,8 +6,6 @@ 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/Ghosts/WorldObjects/QSBGhostBrain.cs b/QSB/EchoesOfTheEye/Ghosts/WorldObjects/QSBGhostBrain.cs index 7ed84448..72d9ef70 100644 --- a/QSB/EchoesOfTheEye/Ghosts/WorldObjects/QSBGhostBrain.cs +++ b/QSB/EchoesOfTheEye/Ghosts/WorldObjects/QSBGhostBrain.cs @@ -21,7 +21,7 @@ public class QSBGhostBrain : WorldObject, IGhostObject public override void SendInitialState(uint to) { - + // todo SendInitialState } public override async UniTask Init(CancellationToken ct) diff --git a/QSB/EchoesOfTheEye/Ghosts/WorldObjects/QSBGhostController.cs b/QSB/EchoesOfTheEye/Ghosts/WorldObjects/QSBGhostController.cs index 6be64ff1..228ad983 100644 --- a/QSB/EchoesOfTheEye/Ghosts/WorldObjects/QSBGhostController.cs +++ b/QSB/EchoesOfTheEye/Ghosts/WorldObjects/QSBGhostController.cs @@ -17,7 +17,7 @@ public class QSBGhostController : WorldObject, IGhostObject { public override void SendInitialState(uint to) { - + // todo SendInitialState } public QSBGhostEffects _effects; diff --git a/QSB/EchoesOfTheEye/Ghosts/WorldObjects/QSBGhostEffects.cs b/QSB/EchoesOfTheEye/Ghosts/WorldObjects/QSBGhostEffects.cs index df1f136d..bb31271f 100644 --- a/QSB/EchoesOfTheEye/Ghosts/WorldObjects/QSBGhostEffects.cs +++ b/QSB/EchoesOfTheEye/Ghosts/WorldObjects/QSBGhostEffects.cs @@ -16,7 +16,7 @@ public class QSBGhostEffects : WorldObject, IGhostObject { public override void SendInitialState(uint to) { - + // todo SendInitialState } public override bool ShouldDisplayDebug() => false; diff --git a/QSB/EchoesOfTheEye/Ghosts/WorldObjects/QSBGhostGrabController.cs b/QSB/EchoesOfTheEye/Ghosts/WorldObjects/QSBGhostGrabController.cs index 804c2922..437eae15 100644 --- a/QSB/EchoesOfTheEye/Ghosts/WorldObjects/QSBGhostGrabController.cs +++ b/QSB/EchoesOfTheEye/Ghosts/WorldObjects/QSBGhostGrabController.cs @@ -11,7 +11,7 @@ public class QSBGhostGrabController : WorldObject { public override void SendInitialState(uint to) { - + // todo SendInitialState } public void GrabPlayer(float speed, GhostPlayer player, bool remote = false) diff --git a/QSB/EchoesOfTheEye/Ghosts/WorldObjects/QSBGhostNodeMap.cs b/QSB/EchoesOfTheEye/Ghosts/WorldObjects/QSBGhostNodeMap.cs index ad91635f..5226c33e 100644 --- a/QSB/EchoesOfTheEye/Ghosts/WorldObjects/QSBGhostNodeMap.cs +++ b/QSB/EchoesOfTheEye/Ghosts/WorldObjects/QSBGhostNodeMap.cs @@ -11,6 +11,6 @@ internal class QSBGhostNodeMap : WorldObject { public override void SendInitialState(uint to) { - + // todo SendInitialState?? } } diff --git a/QSB/EchoesOfTheEye/Ghosts/WorldObjects/QSBGhostSensors.cs b/QSB/EchoesOfTheEye/Ghosts/WorldObjects/QSBGhostSensors.cs index 952f92e0..af8b8d10 100644 --- a/QSB/EchoesOfTheEye/Ghosts/WorldObjects/QSBGhostSensors.cs +++ b/QSB/EchoesOfTheEye/Ghosts/WorldObjects/QSBGhostSensors.cs @@ -15,7 +15,7 @@ public class QSBGhostSensors : WorldObject, IGhostObject { public override void SendInitialState(uint to) { - + // todo SendInitialState } public override string ReturnLabel() => ""; diff --git a/QSB/EchoesOfTheEye/GrappleTotemSync/WorldObjects/QSBGrappleTotem.cs b/QSB/EchoesOfTheEye/GrappleTotemSync/WorldObjects/QSBGrappleTotem.cs index 590ea09c..8703f72e 100644 --- a/QSB/EchoesOfTheEye/GrappleTotemSync/WorldObjects/QSBGrappleTotem.cs +++ b/QSB/EchoesOfTheEye/GrappleTotemSync/WorldObjects/QSBGrappleTotem.cs @@ -2,7 +2,4 @@ namespace QSB.EchoesOfTheEye.GrappleTotemSync.WorldObjects; -public class QSBGrappleTotem : WorldObject -{ - public override void SendInitialState(uint to) { } -} +public class QSBGrappleTotem : WorldObject { } diff --git a/QSB/EchoesOfTheEye/LightSensorSync/Messages/IlluminatedByMessage.cs b/QSB/EchoesOfTheEye/LightSensorSync/Messages/IlluminatedByMessage.cs deleted file mode 100644 index d9cdcfc8..00000000 --- a/QSB/EchoesOfTheEye/LightSensorSync/Messages/IlluminatedByMessage.cs +++ /dev/null @@ -1,26 +0,0 @@ -using QSB.EchoesOfTheEye.LightSensorSync.WorldObjects; -using QSB.Messaging; -using System.Linq; - -namespace QSB.EchoesOfTheEye.LightSensorSync.Messages; - -/// -/// always sent by host -/// -internal class IlluminatedByMessage : QSBWorldObjectMessage -{ - public IlluminatedByMessage(uint[] illuminatedBy) : base(illuminatedBy) { } - - public override void OnReceiveRemote() - { - foreach (var added in Data.Except(WorldObject._illuminatedBy)) - { - WorldObject.SetIlluminated(added, true); - } - - foreach (var removed in WorldObject._illuminatedBy.Except(Data)) - { - WorldObject.SetIlluminated(removed, false); - } - } -} diff --git a/QSB/EchoesOfTheEye/LightSensorSync/Messages/IlluminatingLanternsMessage.cs b/QSB/EchoesOfTheEye/LightSensorSync/Messages/IlluminatingLanternsMessage.cs index 4a347a2b..f557cbf6 100644 --- a/QSB/EchoesOfTheEye/LightSensorSync/Messages/IlluminatingLanternsMessage.cs +++ b/QSB/EchoesOfTheEye/LightSensorSync/Messages/IlluminatingLanternsMessage.cs @@ -14,12 +14,6 @@ internal class IlluminatingLanternsMessage : QSBWorldObjectMessage x.GetWorldObject().AttachedObject)); diff --git a/QSB/EchoesOfTheEye/LightSensorSync/Messages/PlayerIlluminatedByMessage.cs b/QSB/EchoesOfTheEye/LightSensorSync/Messages/PlayerIlluminatedByMessage.cs deleted file mode 100644 index a86b0cba..00000000 --- a/QSB/EchoesOfTheEye/LightSensorSync/Messages/PlayerIlluminatedByMessage.cs +++ /dev/null @@ -1,28 +0,0 @@ -using QSB.Messaging; -using QSB.Player; -using System.Linq; - -namespace QSB.EchoesOfTheEye.LightSensorSync.Messages; - -/// -/// always sent by host -/// -internal class PlayerIlluminatedByMessage : QSBMessage<(uint playerId, uint[] illuminatedBy)> -{ - public PlayerIlluminatedByMessage(uint playerId, uint[] illuminatedBy) : base((playerId, illuminatedBy)) { } - - public override void OnReceiveRemote() - { - var qsbPlayerLightSensor = QSBPlayerManager.GetPlayer(Data.playerId).QSBPlayerLightSensor; - - foreach (var added in Data.illuminatedBy.Except(qsbPlayerLightSensor._illuminatedBy)) - { - qsbPlayerLightSensor.SetIlluminated(added, true); - } - - foreach (var removed in qsbPlayerLightSensor._illuminatedBy.Except(Data.illuminatedBy)) - { - qsbPlayerLightSensor.SetIlluminated(removed, false); - } - } -} diff --git a/QSB/EchoesOfTheEye/LightSensorSync/Messages/PlayerIlluminatingLanternsMessage.cs b/QSB/EchoesOfTheEye/LightSensorSync/Messages/PlayerIlluminatingLanternsMessage.cs index 6e6a7d5a..4cc78e69 100644 --- a/QSB/EchoesOfTheEye/LightSensorSync/Messages/PlayerIlluminatingLanternsMessage.cs +++ b/QSB/EchoesOfTheEye/LightSensorSync/Messages/PlayerIlluminatingLanternsMessage.cs @@ -19,12 +19,6 @@ internal class PlayerIlluminatingLanternsMessage : QSBMessage<(uint playerId, in { var lightSensor = (SingleLightSensor)QSBPlayerManager.GetPlayer(Data.playerId).LightSensor; - if (lightSensor.enabled) - { - // sensor is enabled, so this will already be synced - return; - } - lightSensor._illuminatingDreamLanternList.Clear(); lightSensor._illuminatingDreamLanternList.AddRange( Data.lanterns.Select(x => x.GetWorldObject().AttachedObject)); diff --git a/QSB/EchoesOfTheEye/LightSensorSync/Messages/PlayerSetIlluminatedMessage.cs b/QSB/EchoesOfTheEye/LightSensorSync/Messages/PlayerSetIlluminatedMessage.cs index acd6abfe..09e3f171 100644 --- a/QSB/EchoesOfTheEye/LightSensorSync/Messages/PlayerSetIlluminatedMessage.cs +++ b/QSB/EchoesOfTheEye/LightSensorSync/Messages/PlayerSetIlluminatedMessage.cs @@ -6,8 +6,24 @@ namespace QSB.EchoesOfTheEye.LightSensorSync.Messages; internal class PlayerSetIlluminatedMessage : QSBMessage<(uint playerId, bool illuminated)> { public PlayerSetIlluminatedMessage(uint playerId, bool illuminated) : base((playerId, illuminated)) { } - public override void OnReceiveLocal() => OnReceiveRemote(); - public override void OnReceiveRemote() => - QSBPlayerManager.GetPlayer(Data.playerId).QSBPlayerLightSensor.SetIlluminated(From, Data.illuminated); + public override void OnReceiveRemote() + { + var lightSensor = (SingleLightSensor)QSBPlayerManager.GetPlayer(Data.playerId).LightSensor; + + if (lightSensor._illuminated == Data.illuminated) + { + return; + } + + lightSensor._illuminated = Data.illuminated; + if (Data.illuminated) + { + lightSensor.OnDetectLight.Invoke(); + } + else + { + lightSensor.OnDetectDarkness.Invoke(); + } + } } diff --git a/QSB/EchoesOfTheEye/LightSensorSync/Messages/SetIlluminatedMessage.cs b/QSB/EchoesOfTheEye/LightSensorSync/Messages/SetIlluminatedMessage.cs index c4407f0c..74e517d6 100644 --- a/QSB/EchoesOfTheEye/LightSensorSync/Messages/SetIlluminatedMessage.cs +++ b/QSB/EchoesOfTheEye/LightSensorSync/Messages/SetIlluminatedMessage.cs @@ -6,6 +6,22 @@ namespace QSB.EchoesOfTheEye.LightSensorSync.Messages; internal class SetIlluminatedMessage : QSBWorldObjectMessage { public SetIlluminatedMessage(bool illuminated) : base(illuminated) { } - public override void OnReceiveLocal() => OnReceiveRemote(); - public override void OnReceiveRemote() => WorldObject.SetIlluminated(From, Data); + + public override void OnReceiveRemote() + { + if (WorldObject.AttachedObject._illuminated == Data) + { + return; + } + + WorldObject.AttachedObject._illuminated = Data; + if (Data) + { + WorldObject.AttachedObject.OnDetectLight.Invoke(); + } + else + { + WorldObject.AttachedObject.OnDetectDarkness.Invoke(); + } + } } diff --git a/QSB/EchoesOfTheEye/LightSensorSync/Patches/LightSensorPatches.cs b/QSB/EchoesOfTheEye/LightSensorSync/Patches/LightSensorPatches.cs index 948eca3d..783e7d16 100644 --- a/QSB/EchoesOfTheEye/LightSensorSync/Patches/LightSensorPatches.cs +++ b/QSB/EchoesOfTheEye/LightSensorSync/Patches/LightSensorPatches.cs @@ -1,13 +1,23 @@ using HarmonyLib; +using QSB.AuthoritySync; using QSB.EchoesOfTheEye.LightSensorSync.Messages; using QSB.EchoesOfTheEye.LightSensorSync.WorldObjects; using QSB.Messaging; using QSB.Patches; +using QSB.Player; +using QSB.Tools.FlashlightTool; +using QSB.Tools.ProbeTool; +using QSB.Utility; using QSB.WorldSync; using System.Collections.Generic; using System.Linq; using UnityEngine; +/* + * For those who come here, + * leave while you still can. + */ + namespace QSB.EchoesOfTheEye.LightSensorSync.Patches; [HarmonyPatch(typeof(SingleLightSensor))] @@ -15,92 +25,26 @@ internal class LightSensorPatches : QSBPatch { public override QSBPatchTypes Type => QSBPatchTypes.OnClientConnect; - [HarmonyPrefix] - [HarmonyPatch(nameof(SingleLightSensor.Start))] - private static bool Start(SingleLightSensor __instance) - { - if (!QSBWorldSync.AllObjectsReady) - { - return true; - } - - var isPlayerLightSensor = LightSensorManager.IsPlayerLightSensor(__instance); - var qsbPlayerLightSensor = isPlayerLightSensor ? __instance.GetComponent() : null; - var qsbLightSensor = isPlayerLightSensor ? null : __instance.GetWorldObject(); - - if (__instance._lightDetector != null) - { - __instance._lightSources = new List(); - __instance._lightSourceMask = LightSourceType.VOLUME_ONLY; - if (__instance._detectFlashlight) - { - __instance._lightSourceMask |= LightSourceType.FLASHLIGHT; - } - - if (__instance._detectProbe) - { - __instance._lightSourceMask |= LightSourceType.PROBE; - } - - if (__instance._detectDreamLanterns) - { - __instance._lightSourceMask |= LightSourceType.DREAM_LANTERN; - } - - if (__instance._detectSimpleLanterns) - { - __instance._lightSourceMask |= LightSourceType.SIMPLE_LANTERN; - } - - __instance._lightDetector.OnLightVolumeEnter += __instance.OnLightSourceEnter; - __instance._lightDetector.OnLightVolumeExit += __instance.OnLightSourceExit; - } - else - { - Debug.LogError("LightSensor has no LightSourceDetector", __instance); - } - - if (__instance._sector != null) - { - __instance.enabled = false; - __instance._lightDetector.GetShape().enabled = false; - if (__instance._startIlluminated) - { - if (isPlayerLightSensor) - { - qsbPlayerLightSensor._locallyIlluminated = true; - new PlayerSetIlluminatedMessage(qsbPlayerLightSensor.PlayerId, true).Send(); - } - else - { - qsbLightSensor._locallyIlluminated = true; - qsbLightSensor.OnDetectLocalLight?.Invoke(); - qsbLightSensor.SendMessage(new SetIlluminatedMessage(true)); - } - } - } - - return false; - } - [HarmonyPrefix] [HarmonyPatch(nameof(SingleLightSensor.OnSectorOccupantsUpdated))] private static bool OnSectorOccupantsUpdated(SingleLightSensor __instance) { + if (LightSensorManager.IsPlayerLightSensor(__instance)) + { + return true; + } if (!QSBWorldSync.AllObjectsReady) { return true; } - - var isPlayerLightSensor = LightSensorManager.IsPlayerLightSensor(__instance); - var qsbPlayerLightSensor = isPlayerLightSensor ? __instance.GetComponent() : null; - var qsbLightSensor = isPlayerLightSensor ? null : __instance.GetWorldObject(); + var qsbLightSensor = __instance.GetWorldObject(); var containsAnyOccupants = __instance._sector.ContainsAnyOccupants(DynamicOccupant.Player | DynamicOccupant.Probe); if (containsAnyOccupants && !__instance.enabled) { __instance.enabled = true; __instance._lightDetector.GetShape().enabled = true; + qsbLightSensor.RequestOwnership(); if (__instance._preserveStateWhileDisabled) { __instance._fixedUpdateFrameDelayCount = 10; @@ -110,118 +54,233 @@ internal class LightSensorPatches : QSBPatch { __instance.enabled = false; __instance._lightDetector.GetShape().enabled = false; + qsbLightSensor.ReleaseOwnership(); if (!__instance._preserveStateWhileDisabled) { - if (isPlayerLightSensor) + if (__instance._illuminated) { - if (qsbPlayerLightSensor._locallyIlluminated) + Delay.RunFramesLater(10, () => { - qsbPlayerLightSensor._locallyIlluminated = false; - new PlayerSetIlluminatedMessage(qsbPlayerLightSensor.PlayerId, false).Send(); - } + // no one else took ownership, so we can safely make not illuminated + // ie turn off when no one else is there + if (qsbLightSensor.Owner == 0) + { + __instance._illuminated = false; + __instance.OnDetectDarkness.Invoke(); + qsbLightSensor.SendMessage(new SetIlluminatedMessage(false)); + } + }); } - else + + if (qsbLightSensor._locallyIlluminated) { - if (qsbLightSensor._locallyIlluminated) - { - qsbLightSensor._locallyIlluminated = false; - qsbLightSensor.OnDetectLocalDarkness?.Invoke(); - qsbLightSensor.SendMessage(new SetIlluminatedMessage(false)); - } + qsbLightSensor._locallyIlluminated = false; + qsbLightSensor.OnDetectLocalDarkness?.Invoke(); } } } - return false; } /// /// to prevent allocating a new list every frame /// - private static readonly List _prevIlluminatingDreamLanternList = new(); + private static readonly List _illuminatingDreamLanternList = new(); [HarmonyPrefix] [HarmonyPatch(nameof(SingleLightSensor.ManagedFixedUpdate))] private static bool ManagedFixedUpdate(SingleLightSensor __instance) { + if (LightSensorManager.IsPlayerLightSensor(__instance)) + { + return true; + } if (!QSBWorldSync.AllObjectsReady) { return true; } - - var isPlayerLightSensor = LightSensorManager.IsPlayerLightSensor(__instance); - var qsbPlayerLightSensor = isPlayerLightSensor ? __instance.GetComponent() : null; - var qsbLightSensor = isPlayerLightSensor ? null : __instance.GetWorldObject(); + var qsbLightSensor = __instance.GetWorldObject(); if (__instance._fixedUpdateFrameDelayCount > 0) { __instance._fixedUpdateFrameDelayCount--; + return false; } - + var illuminated = __instance._illuminated; + var locallyIlluminated = qsbLightSensor._locallyIlluminated; if (__instance._illuminatingDreamLanternList != null) { - _prevIlluminatingDreamLanternList.Clear(); - _prevIlluminatingDreamLanternList.AddRange(__instance._illuminatingDreamLanternList); + _illuminatingDreamLanternList.Clear(); + _illuminatingDreamLanternList.AddRange(__instance._illuminatingDreamLanternList); } - - var illuminated = __instance._illuminated; __instance.UpdateIllumination(); - bool locallyIlluminated; - if (isPlayerLightSensor) + if (qsbLightSensor.Owner == QSBPlayerManager.LocalPlayerId) { - locallyIlluminated = qsbPlayerLightSensor._locallyIlluminated; - qsbPlayerLightSensor._locallyIlluminated = __instance._illuminated; - } - else - { - locallyIlluminated = qsbLightSensor._locallyIlluminated; - qsbLightSensor._locallyIlluminated = __instance._illuminated; - } - - __instance._illuminated = illuminated; - - if (isPlayerLightSensor) - { - if (!locallyIlluminated && qsbPlayerLightSensor._locallyIlluminated) + if (!illuminated && __instance._illuminated) { - qsbPlayerLightSensor._locallyIlluminated = true; - new PlayerSetIlluminatedMessage(qsbPlayerLightSensor.PlayerId, true).Send(); - } - else if (locallyIlluminated && !qsbPlayerLightSensor._locallyIlluminated) - { - qsbPlayerLightSensor._locallyIlluminated = false; - new PlayerSetIlluminatedMessage(qsbPlayerLightSensor.PlayerId, false).Send(); - } - } - else - { - if (!locallyIlluminated && qsbLightSensor._locallyIlluminated) - { - qsbLightSensor._locallyIlluminated = true; - qsbLightSensor.OnDetectLocalLight?.Invoke(); + __instance.OnDetectLight.Invoke(); qsbLightSensor.SendMessage(new SetIlluminatedMessage(true)); } - else if (locallyIlluminated && !qsbLightSensor._locallyIlluminated) + else if (illuminated && !__instance._illuminated) { - qsbLightSensor._locallyIlluminated = false; - qsbLightSensor.OnDetectLocalDarkness?.Invoke(); + __instance.OnDetectDarkness.Invoke(); qsbLightSensor.SendMessage(new SetIlluminatedMessage(false)); } - } - - if (__instance._illuminatingDreamLanternList != null - && !__instance._illuminatingDreamLanternList.SequenceEqual(_prevIlluminatingDreamLanternList)) - { - if (isPlayerLightSensor) - { - new PlayerIlluminatingLanternsMessage(qsbPlayerLightSensor.PlayerId, __instance._illuminatingDreamLanternList).Send(); - } - else + if (__instance._illuminatingDreamLanternList != null && + !__instance._illuminatingDreamLanternList.SequenceEqual(_illuminatingDreamLanternList)) { qsbLightSensor.SendMessage(new IlluminatingLanternsMessage(__instance._illuminatingDreamLanternList)); } } + if (!locallyIlluminated && qsbLightSensor._locallyIlluminated) + { + qsbLightSensor.OnDetectLocalLight?.Invoke(); + } + else if (locallyIlluminated && !qsbLightSensor._locallyIlluminated) + { + qsbLightSensor.OnDetectLocalDarkness?.Invoke(); + } + return false; + } + [HarmonyPrefix] + [HarmonyPatch(nameof(SingleLightSensor.UpdateIllumination))] + private static bool UpdateIllumination(SingleLightSensor __instance) + { + if (LightSensorManager.IsPlayerLightSensor(__instance)) + { + return true; + } + if (!QSBWorldSync.AllObjectsReady) + { + return true; + } + var qsbLightSensor = __instance.GetWorldObject(); + + __instance._illuminated = false; + qsbLightSensor._locallyIlluminated = false; + __instance._illuminatingDreamLanternList?.Clear(); + if (__instance._lightSources == null || __instance._lightSources.Count == 0) + { + return false; + } + var sensorWorldPos = __instance.transform.TransformPoint(__instance._localSensorOffset); + var sensorWorldDir = Vector3.zero; + if (__instance._directionalSensor) + { + sensorWorldDir = __instance.transform.TransformDirection(__instance._localDirection).normalized; + } + foreach (var lightSource in __instance._lightSources) + { + if ((__instance._lightSourceMask & lightSource.GetLightSourceType()) == lightSource.GetLightSourceType() && + lightSource.CheckIlluminationAtPoint(sensorWorldPos, __instance._sensorRadius, __instance._maxDistance)) + { + switch (lightSource.GetLightSourceType()) + { + case LightSourceType.UNDEFINED: + { + var owlight = lightSource as OWLight2; + var occludableLight = owlight.GetLight().shadows != LightShadows.None && + owlight.GetLight().shadowStrength > 0.5f; + if (owlight.CheckIlluminationAtPoint(sensorWorldPos, __instance._sensorRadius, __instance._maxDistance) && + !__instance.CheckOcclusion(owlight.transform.position, sensorWorldPos, sensorWorldDir, occludableLight)) + { + __instance._illuminated = true; + } + break; + } + case LightSourceType.FLASHLIGHT: + { + if (lightSource is QSBFlashlight qsbFlashlight) + { + var position = qsbFlashlight.Player.Camera.transform.position; + var vector3 = __instance.transform.position - position; + if (Vector3.Angle(qsbFlashlight.Player.Camera.transform.forward, vector3) <= __instance._maxSpotHalfAngle && + !__instance.CheckOcclusion(position, sensorWorldPos, sensorWorldDir)) + { + __instance._illuminated = true; + } + } + else + { + var position = Locator.GetPlayerCamera().transform.position; + var vector3 = __instance.transform.position - position; + if (Vector3.Angle(Locator.GetPlayerCamera().transform.forward, vector3) <= __instance._maxSpotHalfAngle && + !__instance.CheckOcclusion(position, sensorWorldPos, sensorWorldDir)) + { + __instance._illuminated = true; + qsbLightSensor._locallyIlluminated = true; + } + } + break; + } + case LightSourceType.PROBE: + { + if (lightSource is QSBProbe qsbProbe) + { + var probe = qsbProbe; + if (probe != null && + probe.IsLaunched() && + !probe.IsRetrieving() && + probe.CheckIlluminationAtPoint(sensorWorldPos, __instance._sensorRadius, __instance._maxDistance) && + !__instance.CheckOcclusion(probe.GetLightSourcePosition(), sensorWorldPos, sensorWorldDir)) + { + __instance._illuminated = true; + } + } + else + { + var probe = Locator.GetProbe(); + if (probe != null && + probe.IsLaunched() && + !probe.IsRetrieving() && + probe.CheckIlluminationAtPoint(sensorWorldPos, __instance._sensorRadius, __instance._maxDistance) && + !__instance.CheckOcclusion(probe.GetLightSourcePosition(), sensorWorldPos, sensorWorldDir)) + { + __instance._illuminated = true; + qsbLightSensor._locallyIlluminated = true; + } + } + break; + } + case LightSourceType.DREAM_LANTERN: + { + var dreamLanternController = lightSource as DreamLanternController; + if (dreamLanternController.IsLit() && + dreamLanternController.IsFocused(__instance._lanternFocusThreshold) && + dreamLanternController.CheckIlluminationAtPoint(sensorWorldPos, __instance._sensorRadius, __instance._maxDistance) && + !__instance.CheckOcclusion(dreamLanternController.GetLightPosition(), sensorWorldPos, sensorWorldDir)) + { + __instance._illuminatingDreamLanternList.Add(dreamLanternController); + __instance._illuminated = true; + + var dreamLanternItem = dreamLanternController.GetComponent(); + qsbLightSensor._locallyIlluminated |= QSBPlayerManager.LocalPlayer.HeldItem?.AttachedObject == dreamLanternItem; + } + break; + } + case LightSourceType.SIMPLE_LANTERN: + foreach (var owlight in lightSource.GetLights()) + { + var occludableLight = owlight.GetLight().shadows != LightShadows.None && + owlight.GetLight().shadowStrength > 0.5f; + var maxDistance = Mathf.Min(__instance._maxSimpleLanternDistance, __instance._maxDistance); + if (owlight.CheckIlluminationAtPoint(sensorWorldPos, __instance._sensorRadius, maxDistance) && + !__instance.CheckOcclusion(owlight.transform.position, sensorWorldPos, sensorWorldDir, occludableLight)) + { + __instance._illuminated = true; + + var simpleLanternItem = (SimpleLanternItem)lightSource; + qsbLightSensor._locallyIlluminated |= QSBPlayerManager.LocalPlayer.HeldItem?.AttachedObject == simpleLanternItem; + } + } + break; + case LightSourceType.VOLUME_ONLY: + __instance._illuminated = true; + break; + } + } + } return false; } } diff --git a/QSB/EchoesOfTheEye/LightSensorSync/Patches/PlayerLightSensorPatches.cs b/QSB/EchoesOfTheEye/LightSensorSync/Patches/PlayerLightSensorPatches.cs new file mode 100644 index 00000000..1260ce50 --- /dev/null +++ b/QSB/EchoesOfTheEye/LightSensorSync/Patches/PlayerLightSensorPatches.cs @@ -0,0 +1,197 @@ +using HarmonyLib; +using QSB.EchoesOfTheEye.LightSensorSync.Messages; +using QSB.Messaging; +using QSB.Patches; +using QSB.Player; +using QSB.Tools.FlashlightTool; +using QSB.Tools.ProbeTool; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; + +/* + * For those who come here, + * leave while you still can. + */ + +namespace QSB.EchoesOfTheEye.LightSensorSync.Patches; + +/// +/// remote player light sensors are disabled, so these will only run for the local player light sensor +/// +[HarmonyPatch(typeof(SingleLightSensor))] +internal class PlayerLightSensorPatches : QSBPatch +{ + public override QSBPatchTypes Type => QSBPatchTypes.OnClientConnect; + + /// + /// to prevent allocating a new list every frame + /// + private static readonly List _illuminatingDreamLanternList = new(); + + [HarmonyPrefix] + [HarmonyPatch(nameof(SingleLightSensor.ManagedFixedUpdate))] + private static bool ManagedFixedUpdate(SingleLightSensor __instance) + { + if (!LightSensorManager.IsPlayerLightSensor(__instance)) + { + return true; + } + + if (__instance._fixedUpdateFrameDelayCount > 0) + { + __instance._fixedUpdateFrameDelayCount--; + return false; + } + var illuminated = __instance._illuminated; + if (__instance._illuminatingDreamLanternList != null) + { + _illuminatingDreamLanternList.Clear(); + _illuminatingDreamLanternList.AddRange(__instance._illuminatingDreamLanternList); + } + __instance.UpdateIllumination(); + if (!illuminated && __instance._illuminated) + { + __instance.OnDetectLight.Invoke(); + new PlayerSetIlluminatedMessage(QSBPlayerManager.LocalPlayerId, true).Send(); + } + else if (illuminated && !__instance._illuminated) + { + __instance.OnDetectDarkness.Invoke(); + new PlayerSetIlluminatedMessage(QSBPlayerManager.LocalPlayerId, false).Send(); + } + if (__instance._illuminatingDreamLanternList != null && + !__instance._illuminatingDreamLanternList.SequenceEqual(_illuminatingDreamLanternList)) + { + new PlayerIlluminatingLanternsMessage(QSBPlayerManager.LocalPlayerId, __instance._illuminatingDreamLanternList).Send(); + } + return false; + } + + [HarmonyPrefix] + [HarmonyPatch(nameof(SingleLightSensor.UpdateIllumination))] + private static bool UpdateIllumination(SingleLightSensor __instance) + { + if (!LightSensorManager.IsPlayerLightSensor(__instance)) + { + return true; + } + + __instance._illuminated = false; + __instance._illuminatingDreamLanternList?.Clear(); + if (__instance._lightSources == null || __instance._lightSources.Count == 0) + { + return false; + } + var sensorWorldPos = __instance.transform.TransformPoint(__instance._localSensorOffset); + var sensorWorldDir = Vector3.zero; + if (__instance._directionalSensor) + { + sensorWorldDir = __instance.transform.TransformDirection(__instance._localDirection).normalized; + } + foreach (var lightSource in __instance._lightSources) + { + if ((__instance._lightSourceMask & lightSource.GetLightSourceType()) == lightSource.GetLightSourceType() && + lightSource.CheckIlluminationAtPoint(sensorWorldPos, __instance._sensorRadius, __instance._maxDistance)) + { + switch (lightSource.GetLightSourceType()) + { + case LightSourceType.UNDEFINED: + { + var owlight = lightSource as OWLight2; + var occludableLight = owlight.GetLight().shadows != LightShadows.None && + owlight.GetLight().shadowStrength > 0.5f; + if (owlight.CheckIlluminationAtPoint(sensorWorldPos, __instance._sensorRadius, __instance._maxDistance) && + !__instance.CheckOcclusion(owlight.transform.position, sensorWorldPos, sensorWorldDir, occludableLight)) + { + __instance._illuminated = true; + } + break; + } + case LightSourceType.FLASHLIGHT: + { + if (lightSource is QSBFlashlight qsbFlashlight) + { + var position = qsbFlashlight.Player.Camera.transform.position; + var vector3 = __instance.transform.position - position; + if (Vector3.Angle(qsbFlashlight.Player.Camera.transform.forward, vector3) <= __instance._maxSpotHalfAngle && + !__instance.CheckOcclusion(position, sensorWorldPos, sensorWorldDir)) + { + __instance._illuminated = true; + } + } + else + { + var position = Locator.GetPlayerCamera().transform.position; + var vector3 = __instance.transform.position - position; + if (Vector3.Angle(Locator.GetPlayerCamera().transform.forward, vector3) <= __instance._maxSpotHalfAngle && + !__instance.CheckOcclusion(position, sensorWorldPos, sensorWorldDir)) + { + __instance._illuminated = true; + } + } + break; + } + case LightSourceType.PROBE: + { + if (lightSource is QSBProbe qsbProbe) + { + var probe = qsbProbe; + if (probe != null && + probe.IsLaunched() && + !probe.IsRetrieving() && + probe.CheckIlluminationAtPoint(sensorWorldPos, __instance._sensorRadius, __instance._maxDistance) && + !__instance.CheckOcclusion(probe.GetLightSourcePosition(), sensorWorldPos, sensorWorldDir)) + { + __instance._illuminated = true; + } + } + else + { + var probe = Locator.GetProbe(); + if (probe != null && + probe.IsLaunched() && + !probe.IsRetrieving() && + probe.CheckIlluminationAtPoint(sensorWorldPos, __instance._sensorRadius, __instance._maxDistance) && + !__instance.CheckOcclusion(probe.GetLightSourcePosition(), sensorWorldPos, sensorWorldDir)) + { + __instance._illuminated = true; + } + } + break; + } + case LightSourceType.DREAM_LANTERN: + { + var dreamLanternController = lightSource as DreamLanternController; + if (dreamLanternController.IsLit() && + dreamLanternController.IsFocused(__instance._lanternFocusThreshold) && + dreamLanternController.CheckIlluminationAtPoint(sensorWorldPos, __instance._sensorRadius, __instance._maxDistance) && + !__instance.CheckOcclusion(dreamLanternController.GetLightPosition(), sensorWorldPos, sensorWorldDir)) + { + __instance._illuminatingDreamLanternList.Add(dreamLanternController); + __instance._illuminated = true; + } + break; + } + case LightSourceType.SIMPLE_LANTERN: + foreach (var owlight in lightSource.GetLights()) + { + var occludableLight = owlight.GetLight().shadows != LightShadows.None && + owlight.GetLight().shadowStrength > 0.5f; + var maxDistance = Mathf.Min(__instance._maxSimpleLanternDistance, __instance._maxDistance); + if (owlight.CheckIlluminationAtPoint(sensorWorldPos, __instance._sensorRadius, maxDistance) && + !__instance.CheckOcclusion(owlight.transform.position, sensorWorldPos, sensorWorldDir, occludableLight)) + { + __instance._illuminated = true; + } + } + break; + case LightSourceType.VOLUME_ONLY: + __instance._illuminated = true; + break; + } + } + } + return false; + } +} diff --git a/QSB/EchoesOfTheEye/LightSensorSync/QSBPlayerLightSensor.cs b/QSB/EchoesOfTheEye/LightSensorSync/QSBPlayerLightSensor.cs index 01773432..5c7a7e42 100644 --- a/QSB/EchoesOfTheEye/LightSensorSync/QSBPlayerLightSensor.cs +++ b/QSB/EchoesOfTheEye/LightSensorSync/QSBPlayerLightSensor.cs @@ -2,74 +2,49 @@ using QSB.Messaging; using QSB.Player; using QSB.WorldSync; -using System; -using System.Collections.Generic; using System.Linq; using UnityEngine; +/* + * For those who come here, + * leave while you still can. + */ + namespace QSB.EchoesOfTheEye.LightSensorSync; /// -/// stores a bit of extra data needed for player light sensor sync -/// todo you might be able to remove when you simplify light sensor after the fake sector thingy +/// only purpose is to handle initial state sync. +/// +/// we don't have to worry about start illuminated or sectors. +/// authority is always given to local player light sensor. +/// +/// 2 uses: +/// - AlarmTotem.CheckPlayerVisible +/// - GhostSensors.FixedUpdate_Sensors /// [RequireComponent(typeof(SingleLightSensor))] public class QSBPlayerLightSensor : MonoBehaviour { private SingleLightSensor _lightSensor; - [NonSerialized] - public uint PlayerId; - - internal bool _locallyIlluminated; - internal readonly List _illuminatedBy = new(); + private uint _playerId; private void Awake() { _lightSensor = GetComponent(); - PlayerId = QSBPlayerManager.PlayerList.First(x => x.LightSensor == _lightSensor).PlayerId; + _playerId = QSBPlayerManager.PlayerList.First(x => x.LightSensor == _lightSensor).PlayerId; RequestInitialStatesMessage.SendInitialState += SendInitialState; - QSBPlayerManager.OnRemovePlayer += OnPlayerLeave; } - private void OnDestroy() - { + private void OnDestroy() => RequestInitialStatesMessage.SendInitialState -= SendInitialState; - QSBPlayerManager.OnRemovePlayer -= OnPlayerLeave; - } private void SendInitialState(uint to) { - new PlayerIlluminatedByMessage(PlayerId, _illuminatedBy.ToArray()) { To = to }.Send(); + new PlayerSetIlluminatedMessage(_playerId, _lightSensor._illuminated) { To = to }.Send(); if (_lightSensor._illuminatingDreamLanternList != null) { - new PlayerIlluminatingLanternsMessage(PlayerId, _lightSensor._illuminatingDreamLanternList) { To = to }.Send(); - } - } - - private void OnPlayerLeave(PlayerInfo player) => SetIlluminated(player.PlayerId, false); - - public void SetIlluminated(uint playerId, bool locallyIlluminated) - { - var illuminated = _illuminatedBy.Count > 0; - if (locallyIlluminated) - { - _illuminatedBy.SafeAdd(playerId); - } - else - { - _illuminatedBy.QuickRemove(playerId); - } - - if (!illuminated && _illuminatedBy.Count > 0) - { - _lightSensor._illuminated = true; - _lightSensor.OnDetectLight.Invoke(); - } - else if (illuminated && _illuminatedBy.Count == 0) - { - _lightSensor._illuminated = false; - _lightSensor.OnDetectDarkness.Invoke(); + new PlayerIlluminatingLanternsMessage(_playerId, _lightSensor._illuminatingDreamLanternList) { To = to }.Send(); } } } diff --git a/QSB/EchoesOfTheEye/LightSensorSync/WorldObjects/QSBLightSensor.cs b/QSB/EchoesOfTheEye/LightSensorSync/WorldObjects/QSBLightSensor.cs index 51f3e502..7a4f3ef2 100644 --- a/QSB/EchoesOfTheEye/LightSensorSync/WorldObjects/QSBLightSensor.cs +++ b/QSB/EchoesOfTheEye/LightSensorSync/WorldObjects/QSBLightSensor.cs @@ -1,60 +1,60 @@ using Cysharp.Threading.Tasks; +using QSB.AuthoritySync; using QSB.EchoesOfTheEye.LightSensorSync.Messages; using QSB.Messaging; -using QSB.Player; +using QSB.Utility; using QSB.WorldSync; using System; -using System.Collections.Generic; using System.Threading; +/* + * For those who come here, + * leave while you still can. + */ + namespace QSB.EchoesOfTheEye.LightSensorSync.WorldObjects; /// -/// todo: simplify this after we do fake sectors (we dont need to store a list, we can just do it locally and then sync if it's disabled) +/// BUG: this breaks in zone2. +/// the sector it's enabled in is bigger than the sector the zone2 walls are enabled in :( +/// maybe this can be fixed by making the collision group use the same sector. /// -internal class QSBLightSensor : WorldObject +internal class QSBLightSensor : AuthWorldObject { internal bool _locallyIlluminated; public Action OnDetectLocalLight; public Action OnDetectLocalDarkness; - internal readonly List _illuminatedBy = new(); + + public override bool CanOwn => AttachedObject.enabled; public override void SendInitialState(uint to) { - this.SendMessage(new IlluminatedByMessage(_illuminatedBy.ToArray()) { To = to }); + base.SendInitialState(to); + + this.SendMessage(new SetIlluminatedMessage(AttachedObject._illuminated) { To = to }); if (AttachedObject._illuminatingDreamLanternList != null) { this.SendMessage(new IlluminatingLanternsMessage(AttachedObject._illuminatingDreamLanternList) { To = to }); } } - public override async UniTask Init(CancellationToken ct) => QSBPlayerManager.OnRemovePlayer += OnPlayerLeave; - public override void OnRemoval() => QSBPlayerManager.OnRemovePlayer -= OnPlayerLeave; - private void OnPlayerLeave(PlayerInfo player) => SetIlluminated(player.PlayerId, false); - - public void SetIlluminated(uint playerId, bool locallyIlluminated) + public override async UniTask Init(CancellationToken ct) { - var illuminated = _illuminatedBy.Count > 0; - if (locallyIlluminated) - { - _illuminatedBy.SafeAdd(playerId); - } - else - { - _illuminatedBy.QuickRemove(playerId); - } + await base.Init(ct); - if (!illuminated && _illuminatedBy.Count > 0) + // do this stuff here instead of Start, since world objects won't be ready by that point + Delay.RunWhen(() => QSBWorldSync.AllObjectsReady, () => { - AttachedObject._illuminated = true; - AttachedObject.OnDetectLight.Invoke(); - } - else if (illuminated && _illuminatedBy.Count == 0) - { - AttachedObject._illuminated = false; - AttachedObject.OnDetectDarkness.Invoke(); - } + if (AttachedObject._sector != null) + { + if (AttachedObject._startIlluminated) + { + _locallyIlluminated = true; + OnDetectLocalLight?.Invoke(); + } + } + }); } } diff --git a/QSB/EchoesOfTheEye/Prisoner/Messages/CellevatorCallMessage.cs b/QSB/EchoesOfTheEye/Prisoner/Messages/CellevatorCallMessage.cs index 744fa558..eb7a4d75 100644 --- a/QSB/EchoesOfTheEye/Prisoner/Messages/CellevatorCallMessage.cs +++ b/QSB/EchoesOfTheEye/Prisoner/Messages/CellevatorCallMessage.cs @@ -1,13 +1,10 @@ using QSB.EchoesOfTheEye.Prisoner.WorldObjects; using QSB.Messaging; -using QSB.Patches; namespace QSB.EchoesOfTheEye.Prisoner.Messages; internal class CellevatorCallMessage : QSBWorldObjectMessage { public CellevatorCallMessage(int floorIndex) : base(floorIndex) { } - - public override void OnReceiveRemote() => - QSBPatch.RemoteCall(() => WorldObject.AttachedObject.CallElevatorToFloor(Data)); + public override void OnReceiveRemote() => WorldObject.AttachedObject.CallElevatorToFloor(Data); } diff --git a/QSB/EchoesOfTheEye/Prisoner/Patches/CellevatorPatches.cs b/QSB/EchoesOfTheEye/Prisoner/Patches/CellevatorPatches.cs index 61ba4533..55bb5284 100644 --- a/QSB/EchoesOfTheEye/Prisoner/Patches/CellevatorPatches.cs +++ b/QSB/EchoesOfTheEye/Prisoner/Patches/CellevatorPatches.cs @@ -13,25 +13,26 @@ public class CellevatorPatches : QSBPatch public override QSBPatchTypes Type => QSBPatchTypes.OnClientConnect; [HarmonyPrefix] - [HarmonyPatch(nameof(PrisonCellElevator.CallElevatorToFloor))] - public static void CallElevatorToFloor(PrisonCellElevator __instance, int floorIndex) + [HarmonyPatch(nameof(PrisonCellElevator.CallToTopFloor))] + public static void CallToTopFloor(PrisonCellElevator __instance) { - if (Remote) - { - return; - } - - if (__instance._targetFloorIndex == floorIndex) - { - return; - } - if (!QSBWorldSync.AllObjectsReady) { return; } - __instance.GetWorldObject() - .SendMessage(new CellevatorCallMessage(floorIndex)); + .SendMessage(new CellevatorCallMessage(1)); + } + + [HarmonyPrefix] + [HarmonyPatch(nameof(PrisonCellElevator.CallToBottomFloor))] + public static void CallToBottomFloor(PrisonCellElevator __instance) + { + if (!QSBWorldSync.AllObjectsReady) + { + return; + } + __instance.GetWorldObject() + .SendMessage(new CellevatorCallMessage(0)); } } diff --git a/QSB/EchoesOfTheEye/Prisoner/WorldObjects/QSBPrisonerBrain.cs b/QSB/EchoesOfTheEye/Prisoner/WorldObjects/QSBPrisonerBrain.cs index 2a6debd9..2330eb69 100644 --- a/QSB/EchoesOfTheEye/Prisoner/WorldObjects/QSBPrisonerBrain.cs +++ b/QSB/EchoesOfTheEye/Prisoner/WorldObjects/QSBPrisonerBrain.cs @@ -14,7 +14,7 @@ internal class QSBPrisonerBrain : WorldObject, IGhostObject { public override void SendInitialState(uint to) { - + // todo SendInitialState } public override async UniTask Init(CancellationToken ct) diff --git a/QSB/EchoesOfTheEye/Prisoner/WorldObjects/QSBPrisonerMarker.cs b/QSB/EchoesOfTheEye/Prisoner/WorldObjects/QSBPrisonerMarker.cs index 1134d439..552af46d 100644 --- a/QSB/EchoesOfTheEye/Prisoner/WorldObjects/QSBPrisonerMarker.cs +++ b/QSB/EchoesOfTheEye/Prisoner/WorldObjects/QSBPrisonerMarker.cs @@ -5,7 +5,5 @@ namespace QSB.EchoesOfTheEye.Prisoner.WorldObjects; internal class QSBPrisonerMarker : WorldObject { - public override void SendInitialState(uint to) { } - public Transform Transform => AttachedObject.transform; } diff --git a/QSB/EchoesOfTheEye/QSBRotatingElements.cs b/QSB/EchoesOfTheEye/QSBRotatingElements.cs index 082755ec..05b08c69 100644 --- a/QSB/EchoesOfTheEye/QSBRotatingElements.cs +++ b/QSB/EchoesOfTheEye/QSBRotatingElements.cs @@ -15,8 +15,6 @@ internal abstract class QSBRotatingElements : LinkedWorldObject where T : MonoBehaviour where U : NetworkBehaviour { - public override void SendInitialState(uint to) { } - protected abstract IEnumerable LightSensors { get; } private QSBLightSensor[] _qsbLightSensors; private int _litSensors; diff --git a/QSB/EchoesOfTheEye/RaftSync/TransformSync/RaftTransformSync.cs b/QSB/EchoesOfTheEye/RaftSync/TransformSync/RaftTransformSync.cs index 5950afa9..dc5307d0 100644 --- a/QSB/EchoesOfTheEye/RaftSync/TransformSync/RaftTransformSync.cs +++ b/QSB/EchoesOfTheEye/RaftSync/TransformSync/RaftTransformSync.cs @@ -75,32 +75,30 @@ public class RaftTransformSync : UnsectoredRigidbodySync, ILinkedNetworkBehaviou protected override void ApplyToAttached() { var targetPos = ReferenceTransform.FromRelPos(transform.position); + var targetRot = ReferenceTransform.FromRelRot(transform.rotation); - if (Time.unscaledTime >= _lastSetPositionTime + ForcePositionAfterTime) + var onRaft = Locator.GetPlayerController().GetGroundBody() == AttachedRigidbody; + if (onRaft) { - _lastSetPositionTime = Time.unscaledTime; - - var targetRot = ReferenceTransform.FromRelRot(transform.rotation); - - var onRaft = false; - var localPos = Vector3.zero; - var localRot = Quaternion.identity; - if (Locator.GetPlayerController().GetGroundBody() == AttachedRigidbody) + if (Time.unscaledTime >= _lastSetPositionTime + ForcePositionAfterTime) { - onRaft = true; - localPos = AttachedRigidbody.transform.InverseTransformPoint(Locator.GetPlayerTransform().position); - localRot = AttachedRigidbody.transform.InverseTransformRotation(Locator.GetPlayerTransform().rotation); - } + _lastSetPositionTime = Time.unscaledTime; + var playerBody = Locator.GetPlayerBody(); + var relPos = AttachedTransform.ToRelPos(playerBody.GetPosition()); + var relRot = AttachedTransform.ToRelRot(playerBody.GetRotation()); + + AttachedRigidbody.SetPosition(targetPos); + AttachedRigidbody.SetRotation(targetRot); + + playerBody.SetPosition(AttachedTransform.FromRelPos(relPos)); + playerBody.SetRotation(AttachedTransform.FromRelRot(relRot)); + } + } + else + { AttachedRigidbody.SetPosition(targetPos); AttachedRigidbody.SetRotation(targetRot); - - if (onRaft) - { - var playerTransform = Locator.GetPlayerBody().transform; - playerTransform.position = AttachedRigidbody.transform.TransformPoint(localPos); - playerTransform.rotation = AttachedRigidbody.transform.TransformRotation(localRot); - } } var targetVelocity = ReferenceRigidbody.FromRelVel(Velocity, targetPos); diff --git a/QSB/EchoesOfTheEye/RaftSync/WorldObjects/QSBRaft.cs b/QSB/EchoesOfTheEye/RaftSync/WorldObjects/QSBRaft.cs index ff82c68d..bd1d6ddf 100644 --- a/QSB/EchoesOfTheEye/RaftSync/WorldObjects/QSBRaft.cs +++ b/QSB/EchoesOfTheEye/RaftSync/WorldObjects/QSBRaft.cs @@ -52,9 +52,4 @@ public class QSBRaft : LinkedWorldObject, IQS NetworkBehaviour.netIdentity.UpdateAuthQueue(AuthQueueAction.Force); } } - - public override void SendInitialState(uint to) - { - // not really needed. things work fine without it - } } diff --git a/QSB/EchoesOfTheEye/RaftSync/WorldObjects/QSBRaftDock.cs b/QSB/EchoesOfTheEye/RaftSync/WorldObjects/QSBRaftDock.cs index cc8eba4c..ef1dc465 100644 --- a/QSB/EchoesOfTheEye/RaftSync/WorldObjects/QSBRaftDock.cs +++ b/QSB/EchoesOfTheEye/RaftSync/WorldObjects/QSBRaftDock.cs @@ -8,8 +8,6 @@ public class QSBRaftDock : WorldObject, IQSBDropTarget { IItemDropTarget IQSBDropTarget.AttachedObject => AttachedObject; - public override void SendInitialState(uint to) { } - public void OnPressInteract() => QSBPatch.RemoteCall(AttachedObject.OnPressInteract); } diff --git a/QSB/EchoesOfTheEye/SlideProjectors/WorldObjects/QSBSlideProjector.cs b/QSB/EchoesOfTheEye/SlideProjectors/WorldObjects/QSBSlideProjector.cs index c1b59f8a..292e0bfe 100644 --- a/QSB/EchoesOfTheEye/SlideProjectors/WorldObjects/QSBSlideProjector.cs +++ b/QSB/EchoesOfTheEye/SlideProjectors/WorldObjects/QSBSlideProjector.cs @@ -29,7 +29,6 @@ public class QSBSlideProjector : WorldObject /// public void SetUser(uint user) { - DebugLog.DebugWrite($"{this} - user = {user}"); AttachedObject._interactReceiver.SetInteractionEnabled(user == 0 || user == _user); _user = user; } diff --git a/QSB/EyeOfTheUniverse/InstrumentSync/WorldObjects/QSBQuantumInstrument.cs b/QSB/EyeOfTheUniverse/InstrumentSync/WorldObjects/QSBQuantumInstrument.cs index cde9260c..69ceb8f5 100644 --- a/QSB/EyeOfTheUniverse/InstrumentSync/WorldObjects/QSBQuantumInstrument.cs +++ b/QSB/EyeOfTheUniverse/InstrumentSync/WorldObjects/QSBQuantumInstrument.cs @@ -1,16 +1,10 @@ using QSB.EyeOfTheUniverse.MaskSync; using QSB.WorldSync; -using System.Linq; namespace QSB.EyeOfTheUniverse.InstrumentSync.WorldObjects; internal class QSBQuantumInstrument : WorldObject { - public override void SendInitialState(uint to) - { - // not needed since mid-game join is impossible here - } - public void Gather() { var maskZoneController = QSBWorldSync.GetUnityObject(); @@ -29,4 +23,4 @@ internal class QSBQuantumInstrument : WorldObject AttachedObject.Gather(); } -} \ No newline at end of file +} diff --git a/QSB/EyeOfTheUniverse/Tomb/EyeTombWatcher.cs b/QSB/EyeOfTheUniverse/Tomb/EyeTombWatcher.cs index 7974a771..cdce3755 100644 --- a/QSB/EyeOfTheUniverse/Tomb/EyeTombWatcher.cs +++ b/QSB/EyeOfTheUniverse/Tomb/EyeTombWatcher.cs @@ -7,35 +7,30 @@ namespace QSB.EyeOfTheUniverse.Tomb; internal class EyeTombWatcher : MonoBehaviour { - private EyeTombController tomb; - private bool _observedGrave; + private EyeTombController _tomb; - private void Start() + private void Awake() { - tomb = GetComponent(); - tomb._graveObserveTrigger.OnGainFocus += OnObserveGrave; + _tomb = GetComponent(); + _tomb._graveObserveTrigger.OnGainFocus += OnObserveGrave; + enabled = false; } - private void OnDestroy() - => tomb._graveObserveTrigger.OnGainFocus -= OnObserveGrave; + private void OnDestroy() => + _tomb._graveObserveTrigger.OnGainFocus -= OnObserveGrave; private void OnObserveGrave() { - _observedGrave = true; - tomb._graveObserveTrigger.OnGainFocus -= OnObserveGrave; + _tomb._graveObserveTrigger.OnGainFocus -= OnObserveGrave; + enabled = true; } - + private void FixedUpdate() { - if (!_observedGrave) - { - return; - } - var canShowStage = true; foreach (var player in QSBPlayerManager.PlayerList) { - var playerToStage = tomb._stageRoot.transform.position - player.Body.transform.position; + var playerToStage = _tomb._stageRoot.transform.position - player.Body.transform.position; var playerLookDirection = player.Body.transform.forward; var angle = Vector3.Angle(playerLookDirection, playerToStage); if (angle < 70) @@ -46,9 +41,9 @@ internal class EyeTombWatcher : MonoBehaviour if (canShowStage) { - tomb._stageRoot.SetActive(true); + _tomb._stageRoot.SetActive(true); new ShowStageMessage().Send(); - enabled = false; + Destroy(this); } } } diff --git a/QSB/EyeOfTheUniverse/Tomb/Messages/ShowStageMessage.cs b/QSB/EyeOfTheUniverse/Tomb/Messages/ShowStageMessage.cs index ba1e0e6b..16f404c9 100644 --- a/QSB/EyeOfTheUniverse/Tomb/Messages/ShowStageMessage.cs +++ b/QSB/EyeOfTheUniverse/Tomb/Messages/ShowStageMessage.cs @@ -1,6 +1,6 @@ using QSB.Messaging; -using QSB.Utility; using QSB.WorldSync; +using UnityEngine; namespace QSB.EyeOfTheUniverse.Tomb.Messages; @@ -10,5 +10,6 @@ internal class ShowStageMessage : QSBMessage { var tomb = QSBWorldSync.GetUnityObject(); tomb._stageRoot.SetActive(true); + Object.Destroy(tomb.GetComponent()); } } diff --git a/QSB/EyeOfTheUniverse/Tomb/TombManager.cs b/QSB/EyeOfTheUniverse/Tomb/TombManager.cs index 4e31fd11..bfcb73a5 100644 --- a/QSB/EyeOfTheUniverse/Tomb/TombManager.cs +++ b/QSB/EyeOfTheUniverse/Tomb/TombManager.cs @@ -11,11 +11,8 @@ internal class TombManager : WorldObjectManager public override async UniTask BuildWorldObjects(OWScene scene, CancellationToken ct) { - if (QSBCore.IsHost) - { - // sike!! no worldobjects here - var tomb = QSBWorldSync.GetUnityObject(); - tomb.gameObject.AddComponent(); - } + // sike!! no worldobjects here + var tomb = QSBWorldSync.GetUnityObject(); + tomb.gameObject.AddComponent(); } } diff --git a/QSB/GameVendor.cs b/QSB/GameVendor.cs new file mode 100644 index 00000000..d13fef44 --- /dev/null +++ b/QSB/GameVendor.cs @@ -0,0 +1,13 @@ +using System; + +namespace QSB; + +[Flags] +public enum GameVendor +{ + None = 0, + Epic = 1, + Steam = 2, + Gamepass = 4 +} + diff --git a/QSB/ItemSync/ItemState.cs b/QSB/ItemSync/ItemState.cs new file mode 100644 index 00000000..db974f6a --- /dev/null +++ b/QSB/ItemSync/ItemState.cs @@ -0,0 +1,41 @@ +using QSB.Player; +using UnityEngine; + +namespace QSB.ItemSync; + +/// +/// used for initial state sync. +/// we have to store this separately because it's not saved in the item itself, unfortunately. +/// +public class ItemState +{ + /// + /// if this is false, there's no need to sync initial state for this item + /// + public bool HasBeenInteractedWith; + + public ItemStateType State; + + // on ground + public Transform Parent; + public Vector3 LocalPosition; + public Vector3 WorldPosition => Parent.TransformPoint(LocalPosition); + public Vector3 LocalNormal; + public Vector3 WorldNormal => Parent.TransformDirection(LocalNormal); + public Sector Sector; + public IItemDropTarget CustomDropTarget; + public OWRigidbody Rigidbody; + + // held + public PlayerInfo HoldingPlayer; + + // socketed + public OWItemSocket Socket; +} + +public enum ItemStateType +{ + OnGround, + Held, + Socketed +} diff --git a/QSB/ItemSync/Messages/DropItemMessage.cs b/QSB/ItemSync/Messages/DropItemMessage.cs index 5577f18f..bf550fef 100644 --- a/QSB/ItemSync/Messages/DropItemMessage.cs +++ b/QSB/ItemSync/Messages/DropItemMessage.cs @@ -58,6 +58,14 @@ internal class DropItemMessage : QSBWorldObjectMessage().AttachedObject : null; WorldObject.DropItem(worldPos, worldNormal, parent, sector, customDropTarget); + WorldObject.ItemState.HasBeenInteractedWith = true; + WorldObject.ItemState.State = ItemStateType.OnGround; + WorldObject.ItemState.LocalPosition = Data.localPosition; + WorldObject.ItemState.Parent = parent; + WorldObject.ItemState.LocalNormal = Data.localNormal; + WorldObject.ItemState.Sector = sector; + WorldObject.ItemState.CustomDropTarget = customDropTarget; + WorldObject.ItemState.Rigidbody = parent.GetComponent(); var player = QSBPlayerManager.GetPlayer(From); player.HeldItem = null; diff --git a/QSB/ItemSync/Messages/MoveToCarryMessage.cs b/QSB/ItemSync/Messages/MoveToCarryMessage.cs index 933b2196..225d1df4 100644 --- a/QSB/ItemSync/Messages/MoveToCarryMessage.cs +++ b/QSB/ItemSync/Messages/MoveToCarryMessage.cs @@ -5,13 +5,15 @@ using QSB.Utility; namespace QSB.ItemSync.Messages; -internal class MoveToCarryMessage : QSBWorldObjectMessage +internal class MoveToCarryMessage : QSBWorldObjectMessage { + public MoveToCarryMessage(uint playerHolding) : base(playerHolding) { } + public override void OnReceiveRemote() { WorldObject.StoreLocation(); - var player = QSBPlayerManager.GetPlayer(From); + var player = QSBPlayerManager.GetPlayer(Data); var itemType = WorldObject.GetItemType(); player.HeldItem = WorldObject; @@ -30,6 +32,9 @@ internal class MoveToCarryMessage : QSBWorldObjectMessage }; WorldObject.PickUpItem(itemSocket); + WorldObject.ItemState.HasBeenInteractedWith = true; + WorldObject.ItemState.State = ItemStateType.Held; + WorldObject.ItemState.HoldingPlayer = player; switch (itemType) { diff --git a/QSB/ItemSync/Messages/SocketItemMessage.cs b/QSB/ItemSync/Messages/SocketItemMessage.cs index 73a9abf0..3b824ee9 100644 --- a/QSB/ItemSync/Messages/SocketItemMessage.cs +++ b/QSB/ItemSync/Messages/SocketItemMessage.cs @@ -27,6 +27,9 @@ internal class SocketItemMessage : QSBMessage<(SocketMessageType Type, int Socke var qsbItem = Data.ItemId.GetWorldObject(); qsbItemSocket.PlaceIntoSocket(qsbItem); + qsbItem.ItemState.HasBeenInteractedWith = true; + qsbItem.ItemState.State = ItemStateType.Socketed; + qsbItem.ItemState.Socket = qsbItemSocket.AttachedObject; var player = QSBPlayerManager.GetPlayer(From); player.HeldItem = null; diff --git a/QSB/ItemSync/Patches/ItemToolPatches.cs b/QSB/ItemSync/Patches/ItemToolPatches.cs index 3ae88049..56acc639 100644 --- a/QSB/ItemSync/Patches/ItemToolPatches.cs +++ b/QSB/ItemSync/Patches/ItemToolPatches.cs @@ -20,7 +20,10 @@ internal class ItemToolPatches : QSBPatch { var qsbItem = item.GetWorldObject(); QSBPlayerManager.LocalPlayer.HeldItem = qsbItem; - qsbItem.SendMessage(new MoveToCarryMessage()); + qsbItem.ItemState.HasBeenInteractedWith = true; + qsbItem.ItemState.State = ItemStateType.Held; + qsbItem.ItemState.HoldingPlayer = QSBPlayerManager.LocalPlayer; + qsbItem.SendMessage(new MoveToCarryMessage(QSBPlayerManager.LocalPlayer.PlayerId)); } [HarmonyPrefix] @@ -29,6 +32,9 @@ internal class ItemToolPatches : QSBPatch { var item = __instance._heldItem; QSBPlayerManager.LocalPlayer.HeldItem = null; + var qsbItem = item.GetWorldObject(); + qsbItem.ItemState.State = ItemStateType.Socketed; + qsbItem.ItemState.Socket = socket; new SocketItemMessage(SocketMessageType.Socket, socket, item).Send(); } @@ -38,6 +44,7 @@ internal class ItemToolPatches : QSBPatch { var item = socket.GetSocketedItem(); var qsbItem = item.GetWorldObject(); + qsbItem.ItemState.HasBeenInteractedWith = true; QSBPlayerManager.LocalPlayer.HeldItem = qsbItem; new SocketItemMessage(SocketMessageType.StartUnsocket, socket, item).Send(); } @@ -92,6 +99,14 @@ internal class ItemToolPatches : QSBPatch qsbItem.SendMessage(new DropItemMessage(hit.point, hit.normal, parent, sector, customDropTarget, targetRigidbody)); + qsbItem.ItemState.State = ItemStateType.OnGround; + qsbItem.ItemState.Parent = parent; + qsbItem.ItemState.LocalPosition = parent.InverseTransformPoint(hit.point); + qsbItem.ItemState.LocalNormal = parent.InverseTransformDirection(hit.normal); + qsbItem.ItemState.Sector = sector; + qsbItem.ItemState.CustomDropTarget = customDropTarget; + qsbItem.ItemState.Rigidbody = targetRigidbody; + return false; } } diff --git a/QSB/ItemSync/WorldObjects/Items/IQSBItem.cs b/QSB/ItemSync/WorldObjects/Items/IQSBItem.cs index d43c932f..7162b010 100644 --- a/QSB/ItemSync/WorldObjects/Items/IQSBItem.cs +++ b/QSB/ItemSync/WorldObjects/Items/IQSBItem.cs @@ -5,6 +5,8 @@ namespace QSB.ItemSync.WorldObjects.Items; public interface IQSBItem : IWorldObject { + ItemState ItemState { get; } + ItemType GetItemType(); void PickUpItem(Transform itemSocket); void DropItem(Vector3 position, Vector3 normal, Transform parent, Sector sector, IItemDropTarget customDropTarget); diff --git a/QSB/ItemSync/WorldObjects/Items/QSBItem.cs b/QSB/ItemSync/WorldObjects/Items/QSBItem.cs index 96fe48e3..45503a03 100644 --- a/QSB/ItemSync/WorldObjects/Items/QSBItem.cs +++ b/QSB/ItemSync/WorldObjects/Items/QSBItem.cs @@ -1,5 +1,7 @@ using Cysharp.Threading.Tasks; +using QSB.ItemSync.Messages; using QSB.ItemSync.WorldObjects.Sockets; +using QSB.Messaging; using QSB.Patches; using QSB.Player; using QSB.SectorSync.WorldObjects; @@ -12,12 +14,24 @@ namespace QSB.ItemSync.WorldObjects.Items; public class QSBItem : WorldObject, IQSBItem where T : OWItem { + public ItemState ItemState { get; } = new(); + private Transform _lastParent; private Vector3 _lastPosition; private Quaternion _lastRotation; private QSBSector _lastSector; private QSBItemSocket _lastSocket; + public override string ReturnLabel() + { + return $"{ToString()}" + + $"\r\nState:{ItemState.State}" + + $"\r\nParent:{ItemState.Parent?.name}" + + $"\r\nLocalPosition:{ItemState.LocalPosition}" + + $"\r\nLocalNormal:{ItemState.LocalNormal}" + + $"\r\nHoldingPlayer:{ItemState.HoldingPlayer?.PlayerId}"; + } + public override async UniTask Init(CancellationToken ct) { await UniTask.WaitUntil(() => QSBWorldSync.AllObjectsAdded, cancellationToken: ct); @@ -61,6 +75,7 @@ public class QSBItem : WorldObject, IQSBItem } else { + // TODO at some point we should probably call the proper drop item code to account for funny overrides AttachedObject.transform.parent = _lastParent; AttachedObject.transform.localPosition = _lastPosition; AttachedObject.transform.localRotation = _lastRotation; @@ -72,7 +87,30 @@ public class QSBItem : WorldObject, IQSBItem public override void SendInitialState(uint to) { - // todo SendInitialState + if (!ItemState.HasBeenInteractedWith) + { + return; + } + + switch (ItemState.State) + { + case ItemStateType.Held: + ((IQSBItem)this).SendMessage(new MoveToCarryMessage(ItemState.HoldingPlayer.PlayerId)); + break; + case ItemStateType.Socketed: + new SocketItemMessage(SocketMessageType.Socket, ItemState.Socket, AttachedObject).Send(); + break; + case ItemStateType.OnGround: + ((IQSBItem)this).SendMessage( + new DropItemMessage( + ItemState.WorldPosition, + ItemState.WorldNormal, + ItemState.Parent, + ItemState.Sector, + ItemState.CustomDropTarget, + ItemState.Rigidbody)); + break; + } } public ItemType GetItemType() => AttachedObject.GetItemType(); diff --git a/QSB/ItemSync/WorldObjects/Items/QSBVisionTorchItem.cs b/QSB/ItemSync/WorldObjects/Items/QSBVisionTorchItem.cs index 965212ef..10ffd92a 100644 --- a/QSB/ItemSync/WorldObjects/Items/QSBVisionTorchItem.cs +++ b/QSB/ItemSync/WorldObjects/Items/QSBVisionTorchItem.cs @@ -1,3 +1,6 @@ namespace QSB.ItemSync.WorldObjects.Items; +/// +/// TODO: SYNC THIS SHIT LMAOOOOOO +/// internal class QSBVisionTorchItem : QSBItem { } \ No newline at end of file diff --git a/QSB/ItemSync/WorldObjects/QSBOtherDropTarget.cs b/QSB/ItemSync/WorldObjects/QSBOtherDropTarget.cs index 4e66cf17..de5985c0 100644 --- a/QSB/ItemSync/WorldObjects/QSBOtherDropTarget.cs +++ b/QSB/ItemSync/WorldObjects/QSBOtherDropTarget.cs @@ -17,9 +17,7 @@ public class QSBOtherDropTarget : WorldObject, IQSBDropTarget { if (AttachedObject is not IItemDropTarget) { - throw new ArgumentException("QSBDropTarget.AttachedObject is not an IItemDropTarget!"); + throw new ArgumentException("QSBOtherDropTarget.AttachedObject is not an IItemDropTarget!"); } } - - public override void SendInitialState(uint to) { } } diff --git a/QSB/Localization/Translation.cs b/QSB/Localization/Translation.cs index 2b5df85d..72addba5 100644 --- a/QSB/Localization/Translation.cs +++ b/QSB/Localization/Translation.cs @@ -13,9 +13,12 @@ public class Translation public string ProductUserID; public string Connect; public string Cancel; + public string HostExistingOrNewOrCopy; + public string HostNewOrCopy; public string HostExistingOrNew; public string ExistingSave; public string NewSave; + public string CopySave; public string DisconnectAreYouSure; public string Yes; public string No; @@ -41,5 +44,10 @@ public class Translation public string TimeSyncWaitForAllToReady; public string TimeSyncWaitForAllToDie; public string GalaxyMapEveryoneNotPresent; + public string YouAreDead; + public string WaitingForRespawn; + public string WaitingForAllToDie; + public string AttachToShip; + public string DetachFromShip; public Dictionary DeathMessages; } diff --git a/QSB/Menus/FourChoicePopupMenu.cs b/QSB/Menus/FourChoicePopupMenu.cs new file mode 100644 index 00000000..5a7544cd --- /dev/null +++ b/QSB/Menus/FourChoicePopupMenu.cs @@ -0,0 +1,381 @@ +using System; +using UnityEngine; +using UnityEngine.EventSystems; +using UnityEngine.UI; + +namespace QSB.Menus; + +[RequireComponent(typeof(Canvas))] +public class FourChoicePopupMenu : Menu +{ + public Text _labelText; + public SubmitAction _cancelAction; + public SubmitAction _ok1Action; + public SubmitAction _ok2Action; + public SubmitAction _ok3Action; + public ButtonWithHotkeyImageElement _cancelButton; + public ButtonWithHotkeyImageElement _confirmButton1; + public ButtonWithHotkeyImageElement _confirmButton2; + public ButtonWithHotkeyImageElement _confirmButton3; + public Canvas _rootCanvas; + + protected Canvas _popupCanvas; + protected GameObject _blocker; + protected bool _closeMenuOnOk = true; + protected IInputCommands _ok1Command; + protected IInputCommands _ok2Command; + protected IInputCommands _ok3Command; + protected IInputCommands _cancelCommand; + protected bool _usingGamepad; + + public event PopupConfirmEvent OnPopupConfirm1; + public event PopupConfirmEvent OnPopupConfirm2; + public event PopupConfirmEvent OnPopupConfirm3; + public event PopupValidateEvent OnPopupValidate; + public event PopupCancelEvent OnPopupCancel; + + public override Selectable GetSelectOnActivate() + { + _usingGamepad = OWInput.UsingGamepad(); + return _usingGamepad ? null : _selectOnActivate; + } + + public virtual void SetUpPopup( + string message, + IInputCommands ok1Command, + IInputCommands ok2Command, + IInputCommands ok3Command, + IInputCommands cancelCommand, + ScreenPrompt ok1Prompt, + ScreenPrompt ok2Prompt, + ScreenPrompt ok3Prompt, + ScreenPrompt cancelPrompt, + bool closeMenuOnOk = true, + bool setCancelButtonActive = true) + { + _labelText.text = message; + SetUpPopupCommands(ok1Command, ok2Command, ok3Command, cancelCommand, ok1Prompt, ok2Prompt, ok3Prompt, cancelPrompt); + if (!(_cancelAction != null)) + { + Debug.LogWarning("PopupMenu.SetUpPopup Cancel button not set!"); + return; + } + + _cancelAction.gameObject.SetActive(setCancelButtonActive); + if (setCancelButtonActive) + { + _selectOnActivate = _cancelAction.GetRequiredComponent(); + return; + } + + _selectOnActivate = _ok1Action.GetRequiredComponent(); + } + + public virtual void SetUpPopupCommands( + IInputCommands ok1Command, + IInputCommands ok2Command, + IInputCommands ok3Command, + IInputCommands cancelCommand, + ScreenPrompt ok1Prompt, + ScreenPrompt ok2Prompt, + ScreenPrompt ok3Prompt, + ScreenPrompt cancelPrompt) + { + _ok1Command = ok1Command; + _ok2Command = ok2Command; + _ok3Command = ok3Command; + _cancelCommand = cancelCommand; + _confirmButton1.SetPrompt(ok1Prompt, InputMode.Menu); + _confirmButton2.SetPrompt(ok2Prompt, InputMode.Menu); + _confirmButton3.SetPrompt(ok3Prompt, InputMode.Menu); + _cancelButton.SetPrompt(cancelPrompt, InputMode.Menu); + } + + public virtual void ResetPopup() + { + _labelText.text = ""; + _ok1Command = null; + _ok2Command = null; + _ok3Command = null; + _cancelCommand = null; + _cancelButton.SetPrompt(null, InputMode.Menu); + _confirmButton1.SetPrompt(null, InputMode.Menu); + _confirmButton2.SetPrompt(null, InputMode.Menu); + _confirmButton3.SetPrompt(null, InputMode.Menu); + _selectOnActivate = null; + } + + public virtual void CloseMenuOnOk(bool value) + { + _closeMenuOnOk = value; + } + + public virtual bool EventsHaveListeners() + { + return OnPopupCancel != null + || OnPopupConfirm1 != null + || OnPopupConfirm2 != null + || OnPopupConfirm3 != null; + } + + public override void InitializeMenu() + { + base.InitializeMenu(); + if (_cancelAction != null) + { + _cancelAction.OnSubmitAction += InvokeCancel; + } + + _ok1Action.OnSubmitAction += InvokeOk1; + _ok2Action.OnSubmitAction += InvokeOk2; + _ok3Action.OnSubmitAction += InvokeOk3; + _popupCanvas = gameObject.GetAddComponent(); + _popupCanvas.overrideSorting = true; + _popupCanvas.sortingOrder = 30000; + gameObject.GetAddComponent(); + gameObject.GetAddComponent(); + } + + protected virtual void Update() + { + if (_cancelCommand != null && OWInput.IsNewlyPressed(_cancelCommand, InputMode.All)) + { + InvokeCancel(); + return; + } + + if (_ok1Command != null && OWInput.IsNewlyPressed(_ok1Command, InputMode.All)) + { + InvokeOk1(); + return; + } + + if (_ok2Command != null && OWInput.IsNewlyPressed(_ok2Command, InputMode.All)) + { + InvokeOk2(); + return; + } + + if (_ok3Command != null && OWInput.IsNewlyPressed(_ok3Command, InputMode.All)) + { + InvokeOk3(); + return; + } + + if (_usingGamepad != OWInput.UsingGamepad()) + { + _usingGamepad = OWInput.UsingGamepad(); + if (_usingGamepad) + { + Locator.GetMenuInputModule().SelectOnNextUpdate(null); + return; + } + + Locator.GetMenuInputModule().SelectOnNextUpdate(_selectOnActivate); + } + } + + public override void EnableMenu(bool value) + { + if (value == _enabledMenu) + { + return; + } + + _enabledMenu = value; + if (_enabledMenu && !_initialized) + { + InitializeMenu(); + } + + if (!_addToMenuStackManager) + { + if (_enabledMenu) + { + Activate(); + if (_selectOnActivate != null) + { + var component = _selectOnActivate.GetComponent(); + if (component != null) + { + component.SilenceNextSelectEvent(); + } + + Locator.GetMenuInputModule().SelectOnNextUpdate(_selectOnActivate); + return; + } + } + else + { + Deactivate(false); + } + + return; + } + + if (_enabledMenu) + { + MenuStackManager.SharedInstance.Push(this, true); + return; + } + + if (MenuStackManager.SharedInstance.Peek() == this) + { + MenuStackManager.SharedInstance.Pop(false); + return; + } + + Debug.LogError("Cannot disable Menu unless it is on the top the MenuLayerManager stack. Current menu on top: " + MenuStackManager.SharedInstance.Peek().name); + } + + public override void Activate() + { + base.Activate(); + if (_rootCanvas != null) + { + _blocker = CreateBlocker(_rootCanvas); + } + } + + public override void Deactivate(bool keepPreviousMenuVisible = false) + { + if (_rootCanvas != null) + { + DestroyBlocker(_blocker); + } + + var component = _cancelAction.GetComponent(); + if (component != null) + { + component.ChangeState(UIElementState.NORMAL, true); + } + + component = _ok1Action.GetComponent(); + if (component != null) + { + component.ChangeState(UIElementState.NORMAL, true); + } + + component = _ok2Action.GetComponent(); + if (component != null) + { + component.ChangeState(UIElementState.NORMAL, true); + } + + component = _ok3Action.GetComponent(); + if (component != null) + { + component.ChangeState(UIElementState.NORMAL, true); + } + + base.Deactivate(keepPreviousMenuVisible); + } + + public override void OnCancelEvent(GameObject selectedObj, BaseEventData eventData) + { + base.OnCancelEvent(selectedObj, eventData); + OnPopupCancel?.Invoke(); + } + + protected virtual void InvokeCancel() + { + EnableMenu(false); + OnPopupCancel?.Invoke(); + } + + protected virtual bool Validate() + { + var flag = true; + if (OnPopupValidate != null) + { + var invocationList = OnPopupValidate.GetInvocationList(); + for (var i = 0; i < invocationList.Length; i++) + { + var flag2 = (bool)invocationList[i].DynamicInvoke(Array.Empty()); + flag = flag && flag2; + } + } + + return flag; + } + + protected virtual void InvokeOk1() + { + if (!Validate()) + { + return; + } + + if (_closeMenuOnOk) + { + EnableMenu(false); + } + + OnPopupConfirm1?.Invoke(); + } + + protected virtual void InvokeOk2() + { + if (!Validate()) + { + return; + } + + if (_closeMenuOnOk) + { + EnableMenu(false); + } + + OnPopupConfirm2?.Invoke(); + } + + protected virtual void InvokeOk3() + { + if (!Validate()) + { + return; + } + + if (_closeMenuOnOk) + { + EnableMenu(false); + } + + OnPopupConfirm3?.Invoke(); + } + + protected virtual GameObject CreateBlocker(Canvas rootCanvas) + { + var gameObject = new GameObject("Blocker"); + var rectTransform = gameObject.AddComponent(); + rectTransform.SetParent(rootCanvas.transform, false); + rectTransform.anchorMin = Vector3.zero; + rectTransform.anchorMax = Vector3.one; + rectTransform.sizeDelta = Vector2.zero; + var canvas = gameObject.AddComponent(); + canvas.overrideSorting = true; + canvas.sortingLayerID = _popupCanvas.sortingLayerID; + canvas.sortingOrder = _popupCanvas.sortingOrder - 1; + gameObject.AddComponent(); + var image = gameObject.AddComponent(); + if (Locator.GetUIStyleManager() != null) + { + image.color = Locator.GetUIStyleManager().GetPopupBlockerColor(); + return gameObject; + } + + image.color = Color.clear; + return gameObject; + } + + protected virtual void DestroyBlocker(GameObject blocker) + { + Destroy(blocker); + } + + public delegate void PopupConfirmEvent(); + + public delegate bool PopupValidateEvent(); + + public delegate void PopupCancelEvent(); +} diff --git a/QSB/Menus/MenuManager.cs b/QSB/Menus/MenuManager.cs index 110e53bf..d0178796 100644 --- a/QSB/Menus/MenuManager.cs +++ b/QSB/Menus/MenuManager.cs @@ -3,6 +3,7 @@ using Mirror; using QSB.Localization; using QSB.Messaging; using QSB.Player.TransformSync; +using QSB.SaveSync; using QSB.SaveSync.Messages; using QSB.Utility; using QSB.WorldSync; @@ -33,27 +34,32 @@ internal class MenuManager : MonoBehaviour, IAddComponentOnStart private Button HostButton; private GameObject ConnectButton; private PopupInputMenu ConnectPopup; - private ThreeChoicePopupMenu HostGameTypePopup; + private FourChoicePopupMenu ExistingNewCopyPopup; + private ThreeChoicePopupMenu NewCopyPopup; + private ThreeChoicePopupMenu ExistingNewPopup; private Text _loadingText; private StringBuilder _nowLoadingSB; private const int _titleButtonIndex = 2; private float _connectPopupOpenTime; - private const string UpdateChangelog = $"QSB Version 0.20.2\r\nFixed issues with the Little Scout and attaching/detaching from the ship."; + private const string UpdateChangelog = "QSB Version 0.21.0\r\nMultiplayer saves are now seperate from singleplayer, and items got an overhaul. A lot of bug fixes too."; private Action PopupClose; private bool _intentionalDisconnect; - private GameObject _threeChoicePopupBase; + private GameObject _choicePopupPrefab; public void Start() { Instance = this; - _threeChoicePopupBase = Instantiate(Resources.FindObjectsOfTypeAll().First(x => x.name == "TwoButton-Popup" && x.transform.parent.name == "PopupCanvas" && x.transform.parent.parent.name == "TitleMenu").gameObject); - DontDestroyOnLoad(_threeChoicePopupBase); - _threeChoicePopupBase.SetActive(false); + if (!_choicePopupPrefab) + { + _choicePopupPrefab = Instantiate(Resources.FindObjectsOfTypeAll().First(x => x.name == "TwoButton-Popup" && x.transform.parent?.name == "PopupCanvas" && x.transform.parent?.parent?.name == "TitleMenu").gameObject); + DontDestroyOnLoad(_choicePopupPrefab); + _choicePopupPrefab.SetActive(false); + } MakeTitleMenus(); QSBSceneManager.OnSceneLoaded += OnSceneLoaded; @@ -119,7 +125,7 @@ internal class MenuManager : MonoBehaviour, IAddComponentOnStart { if (QSBSceneManager.CurrentScene != OWScene.TitleScreen) { - DebugLog.ToConsole($"Error - Language changed while not in title screen?! Should be impossible!", OWML.Common.MessageType.Error); + DebugLog.ToConsole("Error - Language changed while not in title screen?! Should be impossible!", OWML.Common.MessageType.Error); return; } @@ -128,7 +134,25 @@ internal class MenuManager : MonoBehaviour, IAddComponentOnStart var text = QSBCore.DebugSettings.UseKcpTransport ? QSBLocalization.Current.PublicIPAddress : QSBLocalization.Current.ProductUserID; ConnectPopup.SetUpPopup(text, InputLibrary.menuConfirm, InputLibrary.cancel, new ScreenPrompt(QSBLocalization.Current.Connect), new ScreenPrompt(QSBLocalization.Current.Cancel), false); ConnectPopup.SetInputFieldPlaceholderText(text); - HostGameTypePopup.SetUpPopup(QSBLocalization.Current.HostExistingOrNew, + ExistingNewCopyPopup.SetUpPopup(QSBLocalization.Current.HostExistingOrNewOrCopy, + InputLibrary.menuConfirm, + InputLibrary.confirm2, + InputLibrary.signalscope, + InputLibrary.cancel, + new ScreenPrompt(QSBLocalization.Current.ExistingSave), + new ScreenPrompt(QSBLocalization.Current.NewSave), + new ScreenPrompt(QSBLocalization.Current.CopySave), + new ScreenPrompt(QSBLocalization.Current.Cancel)); + + NewCopyPopup.SetUpPopup(QSBLocalization.Current.HostNewOrCopy, + InputLibrary.menuConfirm, + InputLibrary.confirm2, + InputLibrary.cancel, + new ScreenPrompt(QSBLocalization.Current.NewSave), + new ScreenPrompt(QSBLocalization.Current.CopySave), + new ScreenPrompt(QSBLocalization.Current.Cancel)); + + ExistingNewPopup.SetUpPopup(QSBLocalization.Current.HostExistingOrNew, InputLibrary.menuConfirm, InputLibrary.confirm2, InputLibrary.cancel, @@ -155,7 +179,7 @@ internal class MenuManager : MonoBehaviour, IAddComponentOnStart public ThreeChoicePopupMenu CreateThreeChoicePopup(string message, string confirm1Text, string confirm2Text, string cancelText) { - var newPopup = Instantiate(_threeChoicePopupBase); + var newPopup = Instantiate(_choicePopupPrefab); switch (LoadManager.GetCurrentScene()) { @@ -170,7 +194,7 @@ internal class MenuManager : MonoBehaviour, IAddComponentOnStart newPopup.transform.localPosition = Vector3.zero; newPopup.transform.localScale = Vector3.one; - newPopup.GetComponentsInChildren().ToList().ForEach(x => Destroy(x)); + newPopup.GetComponentsInChildren().ForEach(Destroy); var originalPopup = newPopup.GetComponent(); @@ -201,9 +225,66 @@ internal class MenuManager : MonoBehaviour, IAddComponentOnStart InputLibrary.cancel, new ScreenPrompt(confirm1Text), new ScreenPrompt(confirm2Text), - new ScreenPrompt(cancelText), - true, - true); + new ScreenPrompt(cancelText)); + return popup; + } + + public FourChoicePopupMenu CreateFourChoicePopup(string message, string confirm1Text, string confirm2Text, string confirm3Text, string cancelText) + { + var newPopup = Instantiate(_choicePopupPrefab); + + switch (LoadManager.GetCurrentScene()) + { + case OWScene.TitleScreen: + newPopup.transform.parent = GameObject.Find("/TitleMenu/PopupCanvas").transform; + break; + case OWScene.SolarSystem: + case OWScene.EyeOfTheUniverse: + newPopup.transform.parent = GameObject.Find("/PauseMenu/PopupCanvas").transform; + break; + } + + newPopup.transform.localPosition = Vector3.zero; + newPopup.transform.localScale = Vector3.one; + newPopup.GetComponentsInChildren().ForEach(Destroy); + + var originalPopup = newPopup.GetComponent(); + + var ok1Button = originalPopup._confirmButton.gameObject; + + var ok2Button = Instantiate(ok1Button, ok1Button.transform.parent); + ok2Button.transform.SetSiblingIndex(1); + + var ok3Button = Instantiate(ok1Button, ok1Button.transform.parent); + ok3Button.transform.SetSiblingIndex(2); + + var popup = newPopup.AddComponent(); + popup._labelText = originalPopup._labelText; + popup._cancelAction = originalPopup._cancelAction; + popup._ok1Action = originalPopup._okAction; + popup._ok2Action = ok2Button.GetComponent(); + popup._ok3Action = ok3Button.GetComponent(); + popup._cancelButton = originalPopup._cancelButton; + popup._confirmButton1 = originalPopup._confirmButton; + popup._confirmButton2 = ok2Button.GetComponent(); + popup._confirmButton3 = ok3Button.GetComponent(); + popup._rootCanvas = originalPopup._rootCanvas; + popup._menuActivationRoot = originalPopup._menuActivationRoot; + popup._startEnabled = originalPopup._startEnabled; + popup._selectOnActivate = originalPopup._selectOnActivate; + popup._selectableItemsRoot = originalPopup._selectableItemsRoot; + popup._subMenus = originalPopup._subMenus; + popup._menuOptions = originalPopup._menuOptions; + popup.SetUpPopup( + message, + InputLibrary.menuConfirm, + InputLibrary.confirm2, + InputLibrary.signalscope, + InputLibrary.cancel, + new ScreenPrompt(confirm1Text), + new ScreenPrompt(confirm2Text), + new ScreenPrompt(confirm3Text), + new ScreenPrompt(cancelText)); return popup; } @@ -289,9 +370,62 @@ internal class MenuManager : MonoBehaviour, IAddComponentOnStart TwoButtonInfoPopup.OnPopupConfirm += () => OnCloseInfoPopup(true); TwoButtonInfoPopup.OnPopupCancel += () => OnCloseInfoPopup(false); - HostGameTypePopup = CreateThreeChoicePopup(QSBLocalization.Current.HostExistingOrNew, QSBLocalization.Current.ExistingSave, QSBLocalization.Current.NewSave, QSBLocalization.Current.Cancel); - HostGameTypePopup.OnPopupConfirm1 += () => Host(false); - HostGameTypePopup.OnPopupConfirm2 += () => Host(true); + ExistingNewCopyPopup = CreateFourChoicePopup(QSBLocalization.Current.HostExistingOrNewOrCopy, + QSBLocalization.Current.ExistingSave, + QSBLocalization.Current.NewSave, + QSBLocalization.Current.CopySave, + QSBLocalization.Current.Cancel); + ExistingNewCopyPopup.OnPopupConfirm1 += () => Host(false); + ExistingNewCopyPopup.OnPopupConfirm2 += () => Host(true); + ExistingNewCopyPopup.OnPopupConfirm3 += () => + { + DebugLog.DebugWrite("Replacing multiplayer save with singleplayer save"); + QSBCore.IsInMultiplayer = true; + + if (QSBCore.IsStandalone) + { + var currentProfile = QSBStandaloneProfileManager.SharedInstance.currentProfile; + QSBStandaloneProfileManager.SharedInstance.SaveGame(currentProfile.gameSave, null, null, null); + } + else + { + var gameSave = QSBMSStoreProfileManager.SharedInstance.currentProfileGameSave; + QSBMSStoreProfileManager.SharedInstance.SaveGame(gameSave, null, null, null); + } + + Host(false); + }; + + NewCopyPopup = CreateThreeChoicePopup(QSBLocalization.Current.HostNewOrCopy, + QSBLocalization.Current.NewSave, + QSBLocalization.Current.CopySave, + QSBLocalization.Current.Cancel); + NewCopyPopup.OnPopupConfirm1 += () => Host(true); + NewCopyPopup.OnPopupConfirm2 += () => + { + DebugLog.DebugWrite("Replacing multiplayer save with singleplayer save"); + QSBCore.IsInMultiplayer = true; + + if (QSBCore.IsStandalone) + { + var currentProfile = QSBStandaloneProfileManager.SharedInstance.currentProfile; + QSBStandaloneProfileManager.SharedInstance.SaveGame(currentProfile.gameSave, null, null, null); + } + else + { + var gameSave = QSBMSStoreProfileManager.SharedInstance.currentProfileGameSave; + QSBMSStoreProfileManager.SharedInstance.SaveGame(gameSave, null, null, null); + } + + Host(false); + }; + + ExistingNewPopup = CreateThreeChoicePopup(QSBLocalization.Current.HostExistingOrNew, + QSBLocalization.Current.ExistingSave, + QSBLocalization.Current.NewSave, + QSBLocalization.Current.Cancel); + ExistingNewPopup.OnPopupConfirm1 += () => Host(false); + ExistingNewPopup.OnPopupConfirm2 += () => Host(true); } private static void SetButtonActive(Button button, bool active) @@ -342,10 +476,10 @@ internal class MenuManager : MonoBehaviour, IAddComponentOnStart DisconnectPopup._labelText.text = popupText; var langController = QSBWorldSync.GetUnityObject().transform.GetChild(0).GetComponent(); - langController.AddTextElement(DisconnectButton.transform.GetChild(0).GetChild(1).GetComponent(), true, true, false); - langController.AddTextElement(DisconnectPopup._labelText, false, true, false); - langController.AddTextElement(DisconnectPopup._confirmButton._buttonText, false, true, false); - langController.AddTextElement(DisconnectPopup._cancelButton._buttonText, false, true, false); + langController.AddTextElement(DisconnectButton.transform.GetChild(0).GetChild(1).GetComponent()); + langController.AddTextElement(DisconnectPopup._labelText, false); + langController.AddTextElement(DisconnectPopup._confirmButton._buttonText, false); + langController.AddTextElement(DisconnectPopup._cancelButton._buttonText, false); } private void MakeTitleMenus() @@ -384,24 +518,35 @@ internal class MenuManager : MonoBehaviour, IAddComponentOnStart } var mainMenuFontController = GameObject.Find("MainMenu").GetComponent(); - mainMenuFontController.AddTextElement(HostButton.transform.GetChild(0).GetChild(1).GetComponent(), true, true, false); - mainMenuFontController.AddTextElement(ConnectButton.transform.GetChild(0).GetChild(1).GetComponent(), true, true, false); + mainMenuFontController.AddTextElement(HostButton.transform.GetChild(0).GetChild(1).GetComponent()); + mainMenuFontController.AddTextElement(ConnectButton.transform.GetChild(0).GetChild(1).GetComponent()); - mainMenuFontController.AddTextElement(OneButtonInfoPopup._labelText, false, true, false); - mainMenuFontController.AddTextElement(OneButtonInfoPopup._confirmButton._buttonText, false, true, false); + mainMenuFontController.AddTextElement(OneButtonInfoPopup._labelText, false); + mainMenuFontController.AddTextElement(OneButtonInfoPopup._confirmButton._buttonText, false); - mainMenuFontController.AddTextElement(TwoButtonInfoPopup._labelText, false, true, false); - mainMenuFontController.AddTextElement(TwoButtonInfoPopup._confirmButton._buttonText, false, true, false); - mainMenuFontController.AddTextElement(TwoButtonInfoPopup._cancelButton._buttonText, false, true, false); + mainMenuFontController.AddTextElement(TwoButtonInfoPopup._labelText, false); + mainMenuFontController.AddTextElement(TwoButtonInfoPopup._confirmButton._buttonText, false); + mainMenuFontController.AddTextElement(TwoButtonInfoPopup._cancelButton._buttonText, false); - mainMenuFontController.AddTextElement(ConnectPopup._labelText, false, true, false); - mainMenuFontController.AddTextElement(ConnectPopup._confirmButton._buttonText, false, true, false); - mainMenuFontController.AddTextElement(ConnectPopup._cancelButton._buttonText, false, true, false); + mainMenuFontController.AddTextElement(ConnectPopup._labelText, false); + mainMenuFontController.AddTextElement(ConnectPopup._confirmButton._buttonText, false); + mainMenuFontController.AddTextElement(ConnectPopup._cancelButton._buttonText, false); - mainMenuFontController.AddTextElement(HostGameTypePopup._labelText, false, true, false); - mainMenuFontController.AddTextElement(HostGameTypePopup._confirmButton1._buttonText, false, true, false); - mainMenuFontController.AddTextElement(HostGameTypePopup._confirmButton2._buttonText, false, true, false); - mainMenuFontController.AddTextElement(HostGameTypePopup._cancelButton._buttonText, false, true, false); + mainMenuFontController.AddTextElement(ExistingNewCopyPopup._labelText, false); + mainMenuFontController.AddTextElement(ExistingNewCopyPopup._confirmButton1._buttonText, false); + mainMenuFontController.AddTextElement(ExistingNewCopyPopup._confirmButton2._buttonText, false); + mainMenuFontController.AddTextElement(ExistingNewCopyPopup._confirmButton3._buttonText, false); + mainMenuFontController.AddTextElement(ExistingNewCopyPopup._cancelButton._buttonText, false); + + mainMenuFontController.AddTextElement(NewCopyPopup._labelText, false); + mainMenuFontController.AddTextElement(NewCopyPopup._confirmButton1._buttonText, false); + mainMenuFontController.AddTextElement(NewCopyPopup._confirmButton2._buttonText, false); + mainMenuFontController.AddTextElement(NewCopyPopup._cancelButton._buttonText, false); + + mainMenuFontController.AddTextElement(ExistingNewPopup._labelText, false); + mainMenuFontController.AddTextElement(ExistingNewPopup._confirmButton1._buttonText, false); + mainMenuFontController.AddTextElement(ExistingNewPopup._confirmButton2._buttonText, false); + mainMenuFontController.AddTextElement(ExistingNewPopup._cancelButton._buttonText, false); } private void Disconnect() @@ -421,23 +566,71 @@ internal class MenuManager : MonoBehaviour, IAddComponentOnStart private void PreHost() { - var doesSaveExist = PlayerData.LoadLoopCount() > 1; - - if (!doesSaveExist) + bool doesSingleplayerSaveExist = false; + bool doesMultiplayerSaveExist = false; + if (!QSBCore.IsStandalone) { - Host(true); - return; + var manager = QSBMSStoreProfileManager.SharedInstance; + doesSingleplayerSaveExist = manager.currentProfileGameSave.loopCount > 1; + doesMultiplayerSaveExist = manager.currentProfileMultiplayerGameSave.loopCount > 1; + } + else + { + var profile = QSBStandaloneProfileManager.SharedInstance.currentProfile; + doesSingleplayerSaveExist = profile.gameSave.loopCount > 1; + doesMultiplayerSaveExist = profile.multiplayerGameSave.loopCount > 1; } - HostGameTypePopup.EnableMenu(true); + if (doesSingleplayerSaveExist && doesMultiplayerSaveExist) + { + DebugLog.DebugWrite("CASE 1 - Both singleplayer and multiplayer saves exist."); + // ask if we want to use the existing multiplayer save, + // start a new multiplayer save, or copy the singleplayer save + ExistingNewCopyPopup.EnableMenu(true); + } + else if (doesSingleplayerSaveExist && !doesMultiplayerSaveExist) + { + DebugLog.DebugWrite("CASE 2 - Only a singleplayer save exists."); + // ask if we want to start a new multiplayer save or copy the singleplayer save + NewCopyPopup.EnableMenu(true); + } + else if (!doesSingleplayerSaveExist && doesMultiplayerSaveExist) + { + DebugLog.DebugWrite("CASE 3 - Only multiplayer save exists."); + // ask if we want to use the existing multiplayer save or start a new one + ExistingNewPopup.EnableMenu(true); + } + else if (!doesSingleplayerSaveExist && !doesMultiplayerSaveExist) + { + DebugLog.DebugWrite("CASE 4 - Neither a singleplayer or a multiplayer save exists."); + // create a new multiplayer save - nothing to copy or load + Host(true); + } } - private void Host(bool newSave) + private void Host(bool newMultiplayerSave) { - if (newSave) + QSBCore.IsInMultiplayer = true; + + if (newMultiplayerSave) { + DebugLog.DebugWrite("Resetting game..."); PlayerData.ResetGame(); } + else + { + DebugLog.DebugWrite("Loading multiplayer game..."); + if (QSBCore.IsStandalone) + { + var profile = QSBStandaloneProfileManager.SharedInstance.currentProfile; + PlayerData.Init(profile.multiplayerGameSave, profile.settingsSave, profile.graphicsSettings, profile.inputJSON); + } + else + { + var manager = QSBMSStoreProfileManager.SharedInstance; + PlayerData.Init(manager.currentProfileMultiplayerGameSave, manager.currentProfileGameSettings, manager.currentProfileGraphicsSettings, manager.currentProfileInputJSON); + } + } _intentionalDisconnect = false; @@ -474,8 +667,20 @@ internal class MenuManager : MonoBehaviour, IAddComponentOnStart private void Connect() { + QSBCore.IsInMultiplayer = true; _intentionalDisconnect = false; + if (QSBCore.IsStandalone) + { + var profile = QSBStandaloneProfileManager.SharedInstance.currentProfile; + PlayerData.Init(profile.multiplayerGameSave, profile.settingsSave, profile.graphicsSettings, profile.inputJSON); + } + else + { + var manager = QSBMSStoreProfileManager.SharedInstance; + PlayerData.Init(manager.currentProfileMultiplayerGameSave, manager.currentProfileGameSettings, manager.currentProfileGraphicsSettings, manager.currentProfileInputJSON); + } + var address = ConnectPopup.GetInputText(); if (address == string.Empty) { @@ -506,6 +711,7 @@ internal class MenuManager : MonoBehaviour, IAddComponentOnStart public void OnKicked(string reason) { + QSBCore.IsInMultiplayer = false; _intentionalDisconnect = true; PopupClose += _ => @@ -521,6 +727,8 @@ internal class MenuManager : MonoBehaviour, IAddComponentOnStart private void OnDisconnected(string error) { + QSBCore.IsInMultiplayer = false; + if (_intentionalDisconnect) { DebugLog.DebugWrite("intentional disconnect. dont show popup"); diff --git a/QSB/Menus/ThreeChoicePopupMenu.cs b/QSB/Menus/ThreeChoicePopupMenu.cs index b6bb40dd..ca56a8d1 100644 --- a/QSB/Menus/ThreeChoicePopupMenu.cs +++ b/QSB/Menus/ThreeChoicePopupMenu.cs @@ -203,7 +203,7 @@ public class ThreeChoicePopupMenu : Menu return; } - Debug.LogError("Cannot disable Menu unless it is on the top the MenuLayerManager stack. Current menu on top: " + MenuStackManager.SharedInstance.Peek().gameObject.name); + Debug.LogError("Cannot disable Menu unless it is on the top the MenuLayerManager stack. Current menu on top: " + MenuStackManager.SharedInstance.Peek().name); } public override void Activate() diff --git a/QSB/Messaging/QSBMessageManager.cs b/QSB/Messaging/QSBMessageManager.cs index 2ba1ab4d..b67976c1 100644 --- a/QSB/Messaging/QSBMessageManager.cs +++ b/QSB/Messaging/QSBMessageManager.cs @@ -72,7 +72,7 @@ public static class QSBMessageManager && player.State is ClientState.AliveInSolarSystem or ClientState.AliveInEye or ClientState.DeadInSolarSystem && msg is not (PlayerInformationMessage or PlayerReadyMessage or RequestStateResyncMessage or ServerStateMessage)) { - DebugLog.ToConsole($"Warning - Got message {msg} from player {msg.From}, but they were not ready. Asking for state resync, just in case.", MessageType.Warning); + //DebugLog.ToConsole($"Warning - Got message {msg} from player {msg.From}, but they were not ready. Asking for state resync, just in case.", MessageType.Warning); new RequestStateResyncMessage().Send(); } } diff --git a/QSB/MeteorSync/Patches/MeteorPatches.cs b/QSB/MeteorSync/Patches/MeteorPatches.cs index d5ba621f..fc583171 100644 --- a/QSB/MeteorSync/Patches/MeteorPatches.cs +++ b/QSB/MeteorSync/Patches/MeteorPatches.cs @@ -13,7 +13,7 @@ namespace QSB.MeteorSync.Patches; /// /// server only /// -public class MeteorServerPatches : QSBPatch +public class ServerMeteorPatches : QSBPatch { public override QSBPatchTypes Type => QSBPatchTypes.OnServerClientConnect; @@ -165,7 +165,7 @@ public class MeteorServerPatches : QSBPatch /// /// client only /// -public class MeteorClientPatches : QSBPatch +public class ClientMeteorPatches : QSBPatch { public override QSBPatchTypes Type => QSBPatchTypes.OnNonServerClientConnect; diff --git a/QSB/MeteorSync/WorldObjects/QSBMeteor.cs b/QSB/MeteorSync/WorldObjects/QSBMeteor.cs index 140f74ea..61f79135 100644 --- a/QSB/MeteorSync/WorldObjects/QSBMeteor.cs +++ b/QSB/MeteorSync/WorldObjects/QSBMeteor.cs @@ -5,11 +5,6 @@ namespace QSB.MeteorSync.WorldObjects; public class QSBMeteor : WorldObject { - public override void SendInitialState(uint to) - { - // we don't really need to sync initial state - } - public static bool IsSpecialImpact(GameObject go) => go == Locator.GetPlayerCollider().gameObject || Locator.GetProbe() != null && go == Locator.GetProbe()._anchor._collider.gameObject; diff --git a/QSB/MeteorSync/WorldObjects/QSBMeteorLauncher.cs b/QSB/MeteorSync/WorldObjects/QSBMeteorLauncher.cs index 03da62d6..f7c98773 100644 --- a/QSB/MeteorSync/WorldObjects/QSBMeteorLauncher.cs +++ b/QSB/MeteorSync/WorldObjects/QSBMeteorLauncher.cs @@ -5,11 +5,6 @@ namespace QSB.MeteorSync.WorldObjects; public class QSBMeteorLauncher : WorldObject { - public override void SendInitialState(uint to) - { - // we don't really need to sync initial state - } - public void PreLaunchMeteor() { foreach (var launchParticle in AttachedObject._launchParticles) diff --git a/QSB/ModelShip/ModelShipManager.cs b/QSB/ModelShip/ModelShipManager.cs index 52e12670..6b60fc9c 100644 --- a/QSB/ModelShip/ModelShipManager.cs +++ b/QSB/ModelShip/ModelShipManager.cs @@ -14,6 +14,14 @@ internal class ModelShipManager : WorldObjectManager public override bool DlcOnly => false; public override async UniTask BuildWorldObjects(OWScene scene, CancellationToken ct) + { + if (QSBCore.IsHost) + { + Instantiate(QSBNetworkManager.singleton.ModelShipPrefab).SpawnWithServerAuthority(); + } + } + + public override void UnbuildWorldObjects() { if (QSBCore.IsHost) { @@ -27,8 +35,6 @@ internal class ModelShipManager : WorldObjectManager NetworkServer.Destroy(ModelShipTransformSync.LocalInstance.gameObject); } - - Instantiate(QSBNetworkManager.singleton.ModelShipPrefab).SpawnWithServerAuthority(); } } -} \ No newline at end of file +} diff --git a/QSB/Patches/QSBPatch.cs b/QSB/Patches/QSBPatch.cs index 0a9d4ebf..42853c67 100644 --- a/QSB/Patches/QSBPatch.cs +++ b/QSB/Patches/QSBPatch.cs @@ -8,6 +8,8 @@ public abstract class QSBPatch { public abstract QSBPatchTypes Type { get; } + public virtual GameVendor PatchVendor => GameVendor.Epic | GameVendor.Steam | GameVendor.Gamepass; + public void DoPatches(Harmony instance) => instance.PatchAll(GetType()); #region remote calls diff --git a/QSB/Patches/QSBPatchManager.cs b/QSB/Patches/QSBPatchManager.cs index 0eddcdfd..e49f88f3 100644 --- a/QSB/Patches/QSBPatchManager.cs +++ b/QSB/Patches/QSBPatchManager.cs @@ -42,7 +42,7 @@ public static class QSBPatchManager OnPatchType?.SafeInvoke(type); //DebugLog.DebugWrite($"Patch block {Enum.GetName(typeof(QSBPatchTypes), type)}", MessageType.Info); - foreach (var patch in _patchList.Where(x => x.Type == type)) + foreach (var patch in _patchList.Where(x => x.Type == type && x.PatchVendor.HasFlag(QSBCore.GameVendor))) { //DebugLog.DebugWrite($" - Patching in {patch.GetType().Name}", MessageType.Info); try diff --git a/QSB/Player/Patches/VolumePatches.cs b/QSB/Player/Patches/VolumePatches.cs index f39e0c64..45f23b93 100644 --- a/QSB/Player/Patches/VolumePatches.cs +++ b/QSB/Player/Patches/VolumePatches.cs @@ -40,4 +40,11 @@ internal class VolumePatches : QSBPatch comp.AddVolume(__instance); } } + + [HarmonyPrefix] + [HarmonyPatch(typeof(ElectricityVolume), nameof(ElectricityVolume.OnEffectVolumeEnter))] + public static bool OnEffectVolumeEnter(ElectricityVolume __instance, GameObject hitObj) => + // this is a dogshit fix to a bug where this would ApplyShock to remote players, + // which would actually apply the shock affects to the entire planet / sector + hitObj.name != "REMOTE_PlayerDetector"; } diff --git a/QSB/Player/PlayerHUDMarker.cs b/QSB/Player/PlayerHUDMarker.cs index 8d242d8a..58c23b29 100644 --- a/QSB/Player/PlayerHUDMarker.cs +++ b/QSB/Player/PlayerHUDMarker.cs @@ -26,6 +26,16 @@ public class PlayerHUDMarker : HUDDistanceMarker _needsInitializing = true; } + private bool ShouldBeVisible() + { + if (_player == null) + { + return false; + } + + return _player.IsReady && !_player.IsDead && !_player.InDreamWorld && _player.Visible; + } + private void Update() { if (_needsInitializing) @@ -42,9 +52,9 @@ public class PlayerHUDMarker : HUDDistanceMarker { var isVisible = _canvasMarker.IsVisible(); - if (_player.Visible != isVisible) + if (ShouldBeVisible() != isVisible) { - _canvasMarker.SetVisibility(_player.Visible); + _canvasMarker.SetVisibility(ShouldBeVisible()); } } else diff --git a/QSB/Player/PlayerInfo.cs b/QSB/Player/PlayerInfo.cs index 12189873..f84c14ce 100644 --- a/QSB/Player/PlayerInfo.cs +++ b/QSB/Player/PlayerInfo.cs @@ -48,6 +48,11 @@ public partial class PlayerInfo /// public void Reset() { + if (AnimationSync != null) + { + AnimationSync.Reset(); + } + EyeState = default; IsDead = default; IsReady = default; @@ -92,7 +97,8 @@ public partial class PlayerInfo else { FlashlightActive = Locator.GetFlashlight()._flashlightOn; - SuitedUp = Locator.GetPlayerBody().GetComponent().IsWearingSuit(); + SuitedUp = Locator.GetPlayerBody().GetComponent().IsWearingSuit() + || QSBSceneManager.CurrentScene == OWScene.EyeOfTheUniverse; } new PlayerInformationMessage().Send(); diff --git a/QSB/Player/PlayerInfoParts/Body.cs b/QSB/Player/PlayerInfoParts/Body.cs index 7bfba9a8..40e3d806 100644 --- a/QSB/Player/PlayerInfoParts/Body.cs +++ b/QSB/Player/PlayerInfoParts/Body.cs @@ -1,5 +1,4 @@ using OWML.Common; -using QSB.EchoesOfTheEye.LightSensorSync; using QSB.Utility; using UnityEngine; @@ -55,6 +54,10 @@ public partial class PlayerInfo } private GameObject _body; + /// + /// remote light sensor is disabled. + /// it only acts as a storage of data and is always synced with the local light sensor. + /// public LightSensor LightSensor { get @@ -74,8 +77,6 @@ public partial class PlayerInfo } } - public QSBPlayerLightSensor QSBPlayerLightSensor; - public Vector3 Velocity { get diff --git a/QSB/Player/PlayerMapMarker.cs b/QSB/Player/PlayerMapMarker.cs index 719f5906..2a5f48f8 100644 --- a/QSB/Player/PlayerMapMarker.cs +++ b/QSB/Player/PlayerMapMarker.cs @@ -4,10 +4,10 @@ namespace QSB.Player; public class PlayerMapMarker : MonoBehaviour { - public string PlayerName; - private Transform _playerTransform; + private PlayerInfo _player; private CanvasMapMarker _canvasMarker; private bool _canvasMarkerInitialized; + private bool _hasBeenSetUpForInit; public void Awake() { @@ -18,7 +18,6 @@ public class PlayerMapMarker : MonoBehaviour public void Start() { enabled = false; - _playerTransform = Locator.GetPlayerTransform(); } public void OnDestroy() @@ -27,22 +26,20 @@ public class PlayerMapMarker : MonoBehaviour GlobalMessenger.RemoveListener("ExitMapView", new Callback(OnExitMapView)); } + public void Init(PlayerInfo player) + { + _player = player; + _hasBeenSetUpForInit = true; + } + public void InitMarker() { var obj = GameObject.FindWithTag("MapCamera"); var markerManager = obj.GetRequiredComponent().GetMarkerManager(); _canvasMarker = markerManager.InstantiateNewMarker(true); - var component = GetComponent(); - if (component != null) - { - markerManager.RegisterMarker(_canvasMarker, component); - } - else - { - markerManager.RegisterMarker(_canvasMarker, transform); - } + markerManager.RegisterMarker(_canvasMarker, transform); - _canvasMarker.SetLabel(PlayerName.ToUpper()); + _canvasMarker.SetLabel(_player.Name.ToUpper()); _canvasMarker.SetColor(Color.white); _canvasMarker.SetVisibility(false); _canvasMarkerInitialized = true; @@ -51,21 +48,31 @@ public class PlayerMapMarker : MonoBehaviour private void OnEnterMapView() => enabled = true; private void OnExitMapView() => enabled = false; + private bool ShouldBeVisible() + { + if (_player == null) + { + return false; + } + + var playerScreenPos = Locator.GetActiveCamera().WorldToScreenPoint(transform.position); + var isInfrontOfCamera = playerScreenPos.z > 0f; + + return _player.IsReady && !_player.IsDead && !_player.InDreamWorld && _player.Visible && isInfrontOfCamera; + } + public void LateUpdate() { - if (!_canvasMarkerInitialized) + if (!_canvasMarkerInitialized && _hasBeenSetUpForInit) { InitMarker(); } - var a = Locator.GetActiveCamera().WorldToScreenPoint(transform.position); - var b = Locator.GetActiveCamera().WorldToScreenPoint(_playerTransform.position); - var vector = a - b; - vector.z = 0f; - var flag = a.z > 0f; - if (_canvasMarker.IsVisible() != flag) + var shouldBeVisible = ShouldBeVisible(); + + if (_canvasMarker.IsVisible() != shouldBeVisible) { - _canvasMarker.SetVisibility(flag); + _canvasMarker.SetVisibility(shouldBeVisible); } } } \ No newline at end of file diff --git a/QSB/Player/QSBPlayerManager.cs b/QSB/Player/QSBPlayerManager.cs index d07c1648..b6d32ade 100644 --- a/QSB/Player/QSBPlayerManager.cs +++ b/QSB/Player/QSBPlayerManager.cs @@ -5,6 +5,7 @@ using QSB.Tools.FlashlightTool; using QSB.Tools.ProbeTool; using QSB.Utility; using System; +using System.Collections; using System.Collections.Generic; using System.Linq; using UnityEngine; @@ -125,4 +126,67 @@ public static class QSBPlayerManager public static IEnumerable<(PlayerInfo Player, IQSBItem HeldItem)> GetPlayerCarryItems() => PlayerList.Select(x => (x, x.HeldItem)); + + private static Dictionary _connectionIdToPlayer = new(); + + public static IEnumerator ValidatePlayers() + { + while (true) + { + if (QSBCore.IsInMultiplayer && QSBCore.IsHost) + { + _connectionIdToPlayer.Clear(); + + var playersToRemove = new List(); + + foreach (var player in PlayerList) + { + var transformSync = player.TransformSync; + + if (transformSync == null) + { + DebugLog.ToConsole($"Error - {player.PlayerId}'s TransformSync is null.", MessageType.Error); + playersToRemove.Add(player); + continue; + } + + var networkIdentity = transformSync.netIdentity; + + if (networkIdentity == null) + { + DebugLog.ToConsole($"Error - {player.PlayerId}'s TransformSync's NetworkIdentity is null.", MessageType.Error); + playersToRemove.Add(player); + continue; + } + + var connectionToClient = networkIdentity.connectionToClient; + + if (_connectionIdToPlayer.ContainsKey(connectionToClient.connectionId)) + { + // oh god oh fuck + DebugLog.ToConsole($"Error - {player.PlayerId}'s connectionToClient.connectionId is already being used?!?", MessageType.Error); + playersToRemove.Add(player); + continue; + } + + _connectionIdToPlayer.Add(connectionToClient.connectionId, player); + } + + if (playersToRemove.Count != 0) + { + DebugLog.DebugWrite($"Removing {playersToRemove.Count} invalid players.", MessageType.Success); + + foreach (var player in playersToRemove) + { + OnRemovePlayer?.Invoke(player); + player.HudMarker?.Remove(); + PlayerList.Remove(player); + DebugLog.DebugWrite($"Remove Invalid Player : {player}", MessageType.Info); + } + } + } + + yield return new WaitForSecondsRealtime(5); + } + } } \ No newline at end of file diff --git a/QSB/Player/TransformSync/PlayerTransformSync.cs b/QSB/Player/TransformSync/PlayerTransformSync.cs index 496e2395..a0e0dcd6 100644 --- a/QSB/Player/TransformSync/PlayerTransformSync.cs +++ b/QSB/Player/TransformSync/PlayerTransformSync.cs @@ -29,8 +29,25 @@ public class PlayerTransformSync : SectoredTransformSync private Transform _visibleStickTip; private Transform _networkStickTip => _networkStickPivot.GetChild(0); + private bool _hasRanOnStartClient; + public override void OnStartClient() { + if (_hasRanOnStartClient) + { + DebugLog.ToConsole($"ERROR - OnStartClient is being called AGAIN for {Player.PlayerId}'s PlayerTransformSync!", MessageType.Error); + return; + } + + _hasRanOnStartClient = true; + if (QSBPlayerManager.PlayerList.Any(x => x.TransformSync == this)) + { + // this really shouldnt happen... + DebugLog.ToConsole($"Error - A PlayerInfo already exists with TransformSync {name}", MessageType.Error); + Destroy(gameObject); // probably bad + return; + } + var player = new PlayerInfo(this); QSBPlayerManager.PlayerList.SafeAdd(player); base.OnStartClient(); @@ -40,7 +57,19 @@ public class PlayerTransformSync : SectoredTransformSync JoinLeaveSingularity.Create(Player, true); } - public override void OnStartLocalPlayer() => LocalInstance = this; + public override void OnStartLocalPlayer() + { + if (LocalInstance != null) + { + DebugLog.ToConsole($"ERROR - LocalInstance is already non-null in OnStartLocalPlayer!", MessageType.Error); + Destroy(gameObject); // probably bad + return; + } + + LocalInstance = this; + } + + public override void OnStopLocalPlayer() => LocalInstance = null; public override void OnStopClient() { diff --git a/QSB/PlayerBodySetup/Local/LocalPlayerCreation.cs b/QSB/PlayerBodySetup/Local/LocalPlayerCreation.cs index 512755ed..ea8deefd 100644 --- a/QSB/PlayerBodySetup/Local/LocalPlayerCreation.cs +++ b/QSB/PlayerBodySetup/Local/LocalPlayerCreation.cs @@ -4,9 +4,7 @@ using QSB.Player; using QSB.Player.Messages; using QSB.SectorSync; using QSB.Tools; -using QSB.Utility; using QSB.WorldSync; -using System.Linq; using UnityEngine; namespace QSB.PlayerBodySetup.Local; @@ -21,8 +19,6 @@ public static class LocalPlayerCreation out Transform visibleStickPivot, out Transform visibleStickTip) { - DebugLog.DebugWrite($"CREATE PLAYER"); - sectorDetector.Init(Locator.GetPlayerSectorDetector()); // player body @@ -38,7 +34,7 @@ public static class LocalPlayerCreation player.CameraBody = cameraBody.gameObject; visibleCameraRoot = cameraBody; - player.QSBPlayerLightSensor = player.LightSensor.gameObject.GetAddComponent(); + player.LightSensor.gameObject.GetAddComponent(); PlayerToolsManager.InitLocal(); diff --git a/QSB/PlayerBodySetup/Remote/RemotePlayerCreation.cs b/QSB/PlayerBodySetup/Remote/RemotePlayerCreation.cs index 06581f6c..2fe62c0f 100644 --- a/QSB/PlayerBodySetup/Remote/RemotePlayerCreation.cs +++ b/QSB/PlayerBodySetup/Remote/RemotePlayerCreation.cs @@ -33,14 +33,10 @@ public static class RemotePlayerCreation out Transform visibleStickPivot, out Transform visibleStickTip) { - DebugLog.DebugWrite($"CREATE PLAYER"); - /* * CREATE PLAYER STRUCTURE */ - DebugLog.DebugWrite($"CREATE PLAYER STRUCTURE"); - // Variable naming convention is broken here to reflect OW unity project (with REMOTE_ prefixed) for readability var REMOTE_Player_Body = Object.Instantiate(GetPrefab()); @@ -54,8 +50,6 @@ public static class RemotePlayerCreation * SET UP PLAYER BODY */ - DebugLog.DebugWrite($"SET UP PLAYER BODY"); - player.Body = REMOTE_Player_Body; player.ThrusterLightTracker = player.Body.GetComponentInChildren(); player.FluidDetector = REMOTE_PlayerDetector.GetComponent(); @@ -63,7 +57,7 @@ public static class RemotePlayerCreation player.AnimationSync.InitRemote(REMOTE_Traveller_HEA_Player_v2.transform); REMOTE_Player_Body.GetComponent().Init(player); - REMOTE_Player_Body.GetComponent().PlayerName = player.Name; + REMOTE_Player_Body.GetComponent().Init(player); player._ditheringAnimator = REMOTE_Player_Body.GetComponent(); player.DreamWorldSpawnAnimator = REMOTE_Player_Body.GetComponent(); player.AudioController = REMOTE_Player_Body.transform.Find("REMOTE_Audio_Player").GetComponent(); @@ -72,15 +66,13 @@ public static class RemotePlayerCreation * SET UP PLAYER CAMERA */ - DebugLog.DebugWrite($"SET UP PLAYER CAMERA"); - REMOTE_PlayerCamera.GetComponent().enabled = false; var owcamera = REMOTE_PlayerCamera.GetComponent(); player.Camera = owcamera; player.CameraBody = REMOTE_PlayerCamera; visibleCameraRoot = REMOTE_PlayerCamera.transform; - player.QSBPlayerLightSensor = player.LightSensor.gameObject.GetAddComponent(); + player.LightSensor.gameObject.GetAddComponent(); PlayerToolsManager.InitRemote(player); @@ -88,8 +80,6 @@ public static class RemotePlayerCreation * SET UP ROASTING STICK */ - DebugLog.DebugWrite($"SET UP ROASTING STICK"); - var REMOTE_Stick_Pivot = REMOTE_Stick_Root.transform.GetChild(0); var mallowRoot = REMOTE_Stick_Pivot.Find("REMOTE_Stick_Tip/Mallow_Root"); var newSystem = mallowRoot.Find("MallowSmoke").gameObject.GetComponent(); diff --git a/QSB/PoolSync/CustomNomaiRemoteCameraPlatform.cs b/QSB/PoolSync/CustomNomaiRemoteCameraPlatform.cs index 9e05a358..14c93d43 100644 --- a/QSB/PoolSync/CustomNomaiRemoteCameraPlatform.cs +++ b/QSB/PoolSync/CustomNomaiRemoteCameraPlatform.cs @@ -728,7 +728,7 @@ internal class CustomNomaiRemoteCameraPlatform : NomaiShared DebugLog.ToConsole($"Warning - {playerId}'s VisibleAnimator is null!", MessageType.Error); } - mirror.Init(player.AnimationSync.VisibleAnimator, hologramCopy.GetChild(0).gameObject.GetComponent()); + mirror.Init(player.AnimationSync.VisibleAnimator, hologramCopy.GetChild(0).gameObject.GetComponent(), null); _playerToHologram.Add(player, hologramCopy.gameObject); diff --git a/QSB/QSB.csproj b/QSB/QSB.csproj index 0891399e..597afd06 100644 --- a/QSB/QSB.csproj +++ b/QSB/QSB.csproj @@ -90,7 +90,7 @@ - + diff --git a/QSB/QSBCore.cs b/QSB/QSBCore.cs index 9dd3efe5..b8b9429f 100644 --- a/QSB/QSBCore.cs +++ b/QSB/QSBCore.cs @@ -5,7 +5,9 @@ using OWML.ModHelper; using QSB.Localization; using QSB.Menus; using QSB.Patches; +using QSB.Player; using QSB.QuantumSync; +using QSB.SaveSync; using QSB.Utility; using QSB.WorldSync; using System; @@ -48,13 +50,18 @@ public class QSBCore : ModBehaviour public static AssetBundle ConversationAssetBundle { get; private set; } public static AssetBundle DebugAssetBundle { get; private set; } public static bool IsHost => NetworkServer.active; - public static bool IsInMultiplayer => QSBNetworkManager.singleton.isNetworkActive; + public static bool IsInMultiplayer; public static string QSBVersion => Helper.Manifest.Version; public static string GameVersion => // ignore the last patch numbers like the title screen does Application.version.Split('.').Take(3).Join(delimiter: "."); public static bool DLCInstalled => EntitlementsManager.IsDlcOwned() == EntitlementsManager.AsyncOwnershipStatus.Owned; public static bool IncompatibleModsAllowed { get; private set; } + public static GameVendor GameVendor { get; private set; } = GameVendor.None; + public static bool IsStandalone => GameVendor is GameVendor.Epic or GameVendor.Steam; + public static IProfileManager ProfileManager => IsStandalone + ? QSBStandaloneProfileManager.SharedInstance + : QSBMSStoreProfileManager.SharedInstance; public static IMenuAPI MenuApi { get; private set; } public static DebugSettings DebugSettings { get; private set; } = new(); public static Storage Storage { get; private set; } = new(); @@ -67,9 +74,42 @@ public class QSBCore : ModBehaviour // incompatible mods "Raicuparta.NomaiVR", "xen.NewHorizons", - "Vesper.AutoResume" + "Vesper.AutoResume", + "Vesper.OuterWildsMMO", + "_nebula.StopTime", + "Leadpogrommer.PeacefulGhosts", + "PacificEngine.OW_Randomizer", + "xen.DayDream" }; + private static void DetermineGameVendor() + { + var gameAssemblyTypes = typeof(AstroObject).Assembly.GetTypes(); + var isEpic = gameAssemblyTypes.Any(x => x.Name == "EpicEntitlementRetriever"); + var isSteam = gameAssemblyTypes.Any(x => x.Name == "SteamEntitlementRetriever"); + var isUWP = gameAssemblyTypes.Any(x => x.Name == "MSStoreEntitlementRetriever"); + + if (isEpic && !isSteam && !isUWP) + { + GameVendor = GameVendor.Epic; + } + else if (!isEpic && isSteam && !isUWP) + { + GameVendor = GameVendor.Steam; + } + else if (!isEpic && !isSteam && isUWP) + { + GameVendor = GameVendor.Gamepass; + } + else + { + // ??? + DebugLog.ToConsole($"FATAL - Could not determine game vendor.", MessageType.Fatal); + } + + DebugLog.DebugWrite($"Determined game vendor as {GameVendor}", MessageType.Info); + } + public void Awake() { EpicRerouter.ModSide.Interop.Go(); @@ -77,6 +117,11 @@ public class QSBCore : ModBehaviour // no, we cant localize this - languages are loaded after the splash screen UIHelper.ReplaceUI(UITextType.PleaseUseController, "Quantum Space Buddies is best experienced with friends..."); + + DetermineGameVendor(); + + QSBPatchManager.Init(); + QSBPatchManager.DoPatchType(QSBPatchTypes.OnModStart); } public void Start() @@ -133,7 +178,6 @@ public class QSBCore : ModBehaviour return; } - QSBPatchManager.Init(); DeterministicManager.Init(); QSBLocalization.Init(); @@ -145,7 +189,7 @@ public class QSBCore : ModBehaviour QSBPatchManager.OnPatchType += OnPatchType; QSBPatchManager.OnUnpatchType += OnUnpatchType; - QSBPatchManager.DoPatchType(QSBPatchTypes.OnModStart); + StartCoroutine(QSBPlayerManager.ValidatePlayers()); } private static void OnPatchType(QSBPatchTypes type) diff --git a/QSB/QSBNetworkManager.cs b/QSB/QSBNetworkManager.cs index acf2cbb2..78fb5359 100644 --- a/QSB/QSBNetworkManager.cs +++ b/QSB/QSBNetworkManager.cs @@ -21,6 +21,7 @@ using QSB.Patches; using QSB.Player; using QSB.Player.Messages; using QSB.Player.TransformSync; +using QSB.SaveSync; using QSB.ShipSync; using QSB.ShipSync.TransformSync; using QSB.Syncs.Occasional; @@ -101,7 +102,7 @@ public class QSBNetworkManager : NetworkManager, IAddComponentOnStart base.Awake(); InitPlayerName(); - StandaloneProfileManager.SharedInstance.OnProfileSignInComplete += _ => InitPlayerName(); + QSBCore.ProfileManager.OnProfileSignInComplete += _ => InitPlayerName(); playerPrefab = QSBCore.NetworkAssetBundle.LoadAsset("Assets/Prefabs/NETWORK_Player_Body.prefab"); playerPrefab.GetRequiredComponent().SetValue("m_AssetId", 1.ToGuid().ToString("N")); @@ -156,15 +157,22 @@ public class QSBNetworkManager : NetworkManager, IAddComponentOnStart { try { - var titleScreenManager = FindObjectOfType(); - var profileManager = titleScreenManager._profileManager; - if (profileManager.GetType().Name == "MSStoreProfileManager") + if (!QSBCore.IsStandalone) { - PlayerName = (string)profileManager.GetType().GetProperty("userDisplayName").GetValue(profileManager); + PlayerName = QSBMSStoreProfileManager.SharedInstance.userDisplayName; } else { - PlayerName = StandaloneProfileManager.SharedInstance.currentProfile.profileName; + var currentProfile = QSBStandaloneProfileManager.SharedInstance.currentProfile; + + if (currentProfile == null) + { + // probably havent created a profile yet + Delay.RunWhen(() => QSBStandaloneProfileManager.SharedInstance.currentProfile != null, () => InitPlayerName()); + return; + } + + PlayerName = currentProfile.profileName; } } catch (Exception ex) diff --git a/QSB/QuantumSync/WorldObjects/QSBQuantumObject.cs b/QSB/QuantumSync/WorldObjects/QSBQuantumObject.cs index f4a3b0f0..9f9011a2 100644 --- a/QSB/QuantumSync/WorldObjects/QSBQuantumObject.cs +++ b/QSB/QuantumSync/WorldObjects/QSBQuantumObject.cs @@ -12,6 +12,11 @@ using UnityEngine; namespace QSB.QuantumSync.WorldObjects; +/// +/// TODO: just use OnSectorOccupantsUpdated instead of this shape bullshit +/// +/// TODO: make it part of the ad-hoc owner interface +/// internal abstract class QSBQuantumObject : WorldObject, IQSBQuantumObject where T : QuantumObject { diff --git a/QSB/QuantumSync/WorldObjects/QSBQuantumSocket.cs b/QSB/QuantumSync/WorldObjects/QSBQuantumSocket.cs index 8529cdbe..5c880afc 100644 --- a/QSB/QuantumSync/WorldObjects/QSBQuantumSocket.cs +++ b/QSB/QuantumSync/WorldObjects/QSBQuantumSocket.cs @@ -5,6 +5,4 @@ namespace QSB.QuantumSync.WorldObjects; internal class QSBQuantumSocket : WorldObject { public override bool ShouldDisplayDebug() => false; - - public override void SendInitialState(uint to) { } } \ No newline at end of file diff --git a/QSB/QuantumSync/WorldObjects/QSBQuantumState.cs b/QSB/QuantumSync/WorldObjects/QSBQuantumState.cs index f7ca85e5..241f9ad1 100644 --- a/QSB/QuantumSync/WorldObjects/QSBQuantumState.cs +++ b/QSB/QuantumSync/WorldObjects/QSBQuantumState.cs @@ -13,6 +13,4 @@ internal class QSBQuantumState : WorldObject } public override bool ShouldDisplayDebug() => false; - - public override void SendInitialState(uint to) { } } \ No newline at end of file diff --git a/QSB/RespawnSync/RespawnHUDMarker.cs b/QSB/RespawnSync/RespawnHUDMarker.cs index a260eef9..f9653a5a 100644 --- a/QSB/RespawnSync/RespawnHUDMarker.cs +++ b/QSB/RespawnSync/RespawnHUDMarker.cs @@ -29,7 +29,7 @@ public class RespawnHUDMarker : HUDDistanceMarker { var isVisible = _canvasMarker.IsVisible(); var shouldBeVisible = RespawnManager.Instance.RespawnNeeded - && !ShipManager.Instance.ShipCockpitUI._shipDamageCtrlr.IsDestroyed(); + && !ShipManager.Instance.IsShipWrecked; if (shouldBeVisible != isVisible) { diff --git a/QSB/RespawnSync/RespawnManager.cs b/QSB/RespawnSync/RespawnManager.cs index ab2aa021..82a84a7d 100644 --- a/QSB/RespawnSync/RespawnManager.cs +++ b/QSB/RespawnSync/RespawnManager.cs @@ -67,6 +67,7 @@ internal class RespawnManager : MonoBehaviour, IAddComponentOnStart return; } + QSBPatchManager.DoUnpatchType(QSBPatchTypes.RespawnTime); QSBPlayerManager.ShowAllPlayers(); QSBPlayerManager.LocalPlayer.UpdateStatesFromObjects(); QSBPlayerManager.PlayerList.ForEach(x => x.IsDead = false); @@ -138,15 +139,16 @@ internal class RespawnManager : MonoBehaviour, IAddComponentOnStart public void OnPlayerDeath(PlayerInfo player) { + player.IsDead = true; + if (_playersPendingRespawn.Contains(player)) { DebugLog.ToConsole($"Warning - Received death message for player who is already in _playersPendingRespawn!", OWML.Common.MessageType.Warning); - return; } - - player.IsDead = true; - - _playersPendingRespawn.Add(player); + else + { + _playersPendingRespawn.Add(player); + } var deadPlayersCount = QSBPlayerManager.PlayerList.Count(x => x.IsDead); @@ -161,15 +163,16 @@ internal class RespawnManager : MonoBehaviour, IAddComponentOnStart public void OnPlayerRespawn(PlayerInfo player) { + player.IsDead = false; + if (!_playersPendingRespawn.Contains(player)) { DebugLog.ToConsole($"Warning - Received respawn message for player who is not in _playersPendingRespawn!", OWML.Common.MessageType.Warning); - return; } - - player.IsDead = false; - - _playersPendingRespawn.Remove(player); + else + { + _playersPendingRespawn.Remove(player); + } player.SetVisible(true, 1); } @@ -177,6 +180,14 @@ internal class RespawnManager : MonoBehaviour, IAddComponentOnStart public void RespawnSomePlayer() { var playerToRespawn = _playersPendingRespawn.First(); + + if (!playerToRespawn.IsDead) + { + DebugLog.ToConsole($"Warning - Tried to respawn player {playerToRespawn.PlayerId} who isn't dead!", OWML.Common.MessageType.Warning); + _playersPendingRespawn.Remove(playerToRespawn); + return; + } + new PlayerRespawnMessage(playerToRespawn.PlayerId).Send(); } } \ No newline at end of file diff --git a/QSB/RespawnSync/ShipRecoveryPoint.cs b/QSB/RespawnSync/ShipRecoveryPoint.cs index ce1ca50a..f42bc528 100644 --- a/QSB/RespawnSync/ShipRecoveryPoint.cs +++ b/QSB/RespawnSync/ShipRecoveryPoint.cs @@ -59,7 +59,7 @@ internal class ShipRecoveryPoint : MonoBehaviour _playerResources = Locator.GetPlayerTransform().GetComponent(); } - if (RespawnManager.Instance.RespawnNeeded && !ShipManager.Instance.ShipCockpitUI._shipDamageCtrlr.IsDestroyed()) + if (RespawnManager.Instance.RespawnNeeded && !ShipManager.Instance.IsShipWrecked) { _interactVolume.EnableSingleInteraction(true, _respawnIndex); _interactVolume.SetKeyCommandVisible(true, _respawnIndex); @@ -127,7 +127,7 @@ internal class ShipRecoveryPoint : MonoBehaviour } else if (inputCommand == _interactVolume.GetInteractionAt(_respawnIndex).inputCommand) { - if (!RespawnManager.Instance.RespawnNeeded || ShipManager.Instance.ShipCockpitUI._shipDamageCtrlr.IsDestroyed()) + if (!RespawnManager.Instance.RespawnNeeded || ShipManager.Instance.IsShipWrecked) { return; } diff --git a/QSB/SaveSync/Patches/InGameProfileMenuManagerPatches.cs b/QSB/SaveSync/Patches/InGameProfileMenuManagerPatches.cs new file mode 100644 index 00000000..f57636c9 --- /dev/null +++ b/QSB/SaveSync/Patches/InGameProfileMenuManagerPatches.cs @@ -0,0 +1,33 @@ +using HarmonyLib; +using QSB.Patches; + +namespace QSB.SaveSync.Patches; + +[HarmonyPatch(typeof(InGameProfileMenuManager))] +internal class InGameProfileMenuManagerPatches : QSBPatch +{ + public override QSBPatchTypes Type => QSBPatchTypes.OnModStart; + + [HarmonyPrefix] + [HarmonyPatch(nameof(InGameProfileMenuManager.InitializeOnAwake))] + public static bool InitializeOnAwake(InGameProfileMenuManager __instance) + { + if (!__instance._initialized) + { + TextTranslation.Get().OnLanguageChanged += __instance.UpdateLanguage; + __instance.UpdateLanguage(); + __instance._profileManager = QSBCore.ProfileManager; + __instance._profileManager.OnProfileSignInComplete += __instance.OnProfileSignInComplete; + __instance._profileManager.OnProfileSignOutComplete += __instance.OnProfileSignOutComplete; + __instance._profileManager.OnProfileReadDone += __instance.OnProfileReadDone; + __instance._returnToGameSubmitAction.OnSubmitAction += __instance.OnResumeGameBtnSubmit; + __instance._returnToTitleSubmitAction.OnSubmitAction += __instance.OnTitleSubmitAction; + LoadManager.OnStartSceneLoad += __instance.OnStartSceneLoad; + LoadManager.OnCompleteSceneLoad += __instance.OnCompleteSceneLoad; + GlobalMessenger.AddListener("PlayerResurrection", new Callback(__instance.OnPlayerResurrection)); + __instance._initialized = true; + } + + return false; + } +} diff --git a/QSB/SaveSync/Patches/PlayerDataPatches.cs b/QSB/SaveSync/Patches/PlayerDataPatches.cs new file mode 100644 index 00000000..bdf88b09 --- /dev/null +++ b/QSB/SaveSync/Patches/PlayerDataPatches.cs @@ -0,0 +1,54 @@ +using HarmonyLib; +using QSB.Patches; +using UnityEngine; + +namespace QSB.SaveSync.Patches; + +[HarmonyPatch(typeof(PlayerData))] +internal class PlayerDataPatches : QSBPatch +{ + public override QSBPatchTypes Type => QSBPatchTypes.OnModStart; + + [HarmonyPrefix] + [HarmonyPatch(nameof(PlayerData.ResetGame))] + public static bool ResetGame() + { + PlayerData._currentGameSave = new GameSave(); + QSBCore.ProfileManager.SaveGame(PlayerData._currentGameSave, null, null, null); + return false; + } + + [HarmonyPrefix] + [HarmonyPatch(nameof(PlayerData.SaveCurrentGame))] + public static bool SaveCurrentGame() + { + PlayerData._currentGameSave.version = Application.version; + QSBCore.ProfileManager.SaveGame(PlayerData._currentGameSave, null, null, null); + return false; + } + + [HarmonyPrefix] + [HarmonyPatch(nameof(PlayerData.SaveInputSettings))] + public static bool SaveInputSettings() + { + QSBCore.ProfileManager.SaveGame(null, null, null, PlayerData.inputJSON); + return false; + } + + [HarmonyPrefix] + [HarmonyPatch(nameof(PlayerData.SaveSettings))] + public static bool SaveSettings() + { + QSBCore.ProfileManager.SaveGame(null, PlayerData._settingsSave, PlayerData._graphicsSettings, PlayerData.inputJSON); + return false; + } + + // this is actually still StandaloneProfileManager in the gamepass dll. game bug? + [HarmonyPrefix] + [HarmonyPatch(nameof(PlayerData.IsBusy))] + public static bool IsBusy(ref bool __result) + { + __result = QSBCore.ProfileManager.isBusyWithFileOps; + return false; + } +} diff --git a/QSB/SaveSync/Patches/ProfileManagerUpdaterPatches.cs b/QSB/SaveSync/Patches/ProfileManagerUpdaterPatches.cs new file mode 100644 index 00000000..da1d832b --- /dev/null +++ b/QSB/SaveSync/Patches/ProfileManagerUpdaterPatches.cs @@ -0,0 +1,19 @@ +using HarmonyLib; +using QSB.Patches; + +namespace QSB.SaveSync.Patches; + +[HarmonyPatch(typeof(ProfileManagerUpdater))] +internal class ProfileManagerUpdaterPatches : QSBPatch +{ + public override QSBPatchTypes Type => QSBPatchTypes.OnModStart; + + [HarmonyPrefix] + [HarmonyPatch(nameof(ProfileManagerUpdater.Start))] + public static bool Start(ProfileManagerUpdater __instance) + { + __instance._profileManager = QSBCore.ProfileManager; + __instance.enabled = true; + return false; + } +} diff --git a/QSB/SaveSync/Patches/ProfileMenuManagerPatches.cs b/QSB/SaveSync/Patches/ProfileMenuManagerPatches.cs new file mode 100644 index 00000000..73948444 --- /dev/null +++ b/QSB/SaveSync/Patches/ProfileMenuManagerPatches.cs @@ -0,0 +1,212 @@ +using HarmonyLib; +using QSB.Patches; +using System.Collections.Generic; +using UnityEngine; +using UnityEngine.UI; + +namespace QSB.SaveSync.Patches; + +[HarmonyPatch(typeof(ProfileMenuManager))] +internal class ProfileMenuManagerPatches : QSBPatch +{ + public override QSBPatchTypes Type => QSBPatchTypes.OnModStart; + public override GameVendor PatchVendor => GameVendor.Epic | GameVendor.Steam; + + [HarmonyPrefix] + [HarmonyPatch(nameof(ProfileMenuManager.OnCreateProfileConfirm))] + public static bool OnCreateProfileConfirm(ProfileMenuManager __instance) + { + __instance._inputPopupActivated = false; + var inputPopup = __instance._createProfileAction.GetInputPopup(); + inputPopup.OnPopupValidate -= __instance.OnCreateProfileValidate; + inputPopup.OnInputPopupValidateChar -= __instance.OnValidateChar; + __instance._createProfileAction.OnSubmitAction -= __instance.OnCreateProfileConfirm; + QSBStandaloneProfileManager.SharedInstance.TryCreateProfile(__instance._createProfileAction.GetInputString()); + inputPopup.CloseMenuOnOk(true); + __instance.PopulateProfiles(); + __instance.SetCurrentProfileLabel(); + inputPopup.EnableMenu(false); + if (__instance._firstTimeProfileCreation) + { + __instance._firstTimeProfileCreation = false; + __instance.UpdatePopupPrompts(); + } + + return false; + } + + [HarmonyPrefix] + [HarmonyPatch(nameof(ProfileMenuManager.OnCreateProfileValidate))] + public static bool OnCreateProfileValidate(ProfileMenuManager __instance, ref bool __result) + { + var inputPopup = __instance._createProfileAction.GetInputPopup(); + __result = QSBStandaloneProfileManager.SharedInstance.ValidateProfileName(inputPopup.GetInputText()); + return false; + } + + [HarmonyPrefix] + [HarmonyPatch(nameof(ProfileMenuManager.OnDeleteProfile))] + public static bool OnDeleteProfile(ProfileMenuManager __instance) + { + if (__instance._lastSelectedProfileAction != null) + { + __instance._deleteProfileConfirmPopup = null; + QSBStandaloneProfileManager.SharedInstance.DeleteProfile(__instance._lastSelectedProfileAction.GetLabelText()); + __instance.PopulateProfiles(); + __instance._lastSelectedProfileAction = null; + Locator.GetMenuInputModule().SelectOnNextUpdate(__instance._createProfileButton); + } + + return false; + } + + [HarmonyPrefix] + [HarmonyPatch(nameof(ProfileMenuManager.OnSwitchProfile))] + public static bool OnSwitchProfile(ProfileMenuManager __instance) + { + if (__instance._lastSelectedProfileAction != null) + { + __instance._switchProfileConfirmPopup = null; + if (QSBStandaloneProfileManager.SharedInstance.SwitchProfile(__instance._lastSelectedProfileAction.GetLabelText())) + { + __instance.PopulateProfiles(); + __instance.SetCurrentProfileLabel(); + __instance._lastSelectedProfileAction = null; + Locator.GetMenuInputModule().SelectOnNextUpdate(__instance._createProfileButton); + return false; + } + + QSBStandaloneProfileManager.SharedInstance.OnBackupDataRestored += __instance.OnSwitchProfileDataRecoveryCompleted; + } + + return false; + } + + [HarmonyPrefix] + [HarmonyPatch(nameof(ProfileMenuManager.OnSwitchProfileDataRecoveryCompleted))] + public static bool OnSwitchProfileDataRecoveryCompleted(ProfileMenuManager __instance) + { + QSBStandaloneProfileManager.SharedInstance.OnBackupDataRestored -= __instance.OnSwitchProfileDataRecoveryCompleted; + __instance.PopulateProfiles(); + __instance.SetCurrentProfileLabel(); + __instance._lastSelectedProfileAction = null; + Locator.GetMenuInputModule().SelectOnNextUpdate(__instance._createProfileButton); + return false; + } + + [HarmonyPrefix] + [HarmonyPatch(nameof(ProfileMenuManager.OnValidateChar))] + public static bool OnValidateChar(ProfileMenuManager __instance, char c, ref bool __result) + { + __result = __instance._createProfileAction.GetInputPopup().GetInputText().Length < QSBStandaloneProfileManager.SharedInstance.profileNameCharacterLimit + && QSBStandaloneProfileManager.SharedInstance.IsValidCharacterForProfileName(c); + return false; + } + + [HarmonyPrefix] + [HarmonyPatch(nameof(ProfileMenuManager.PopulateProfiles))] + public static bool PopulateProfiles(ProfileMenuManager __instance) + { + if (__instance._listProfileElements == null) + { + __instance._listProfileElements = new List(); + } + else + { + for (int i = 0; i < __instance._listProfileElements.Count; i++) + { + TwoButtonActionElement requiredComponent = __instance._listProfileElements[i].GetRequiredComponent(); + __instance.ClearProfileElementListeners(requiredComponent); + Object.Destroy(__instance._listProfileElements[i]); + } + __instance._listProfileElements.Clear(); + } + + if (__instance._listProfileUIElementLookup == null) + { + __instance._listProfileUIElementLookup = new List(); + } + else + { + __instance._listProfileUIElementLookup.Clear(); + } + + var array = QSBStandaloneProfileManager.SharedInstance.profiles.ToArray(); + var profileName = QSBStandaloneProfileManager.SharedInstance.currentProfile.profileName; + var num = 0; + Selectable selectable = null; + for (int j = 0; j < array.Length; j++) + { + if (!(array[j].profileName == profileName)) + { + GameObject gameObject = Object.Instantiate(__instance._profileItemTemplate); + gameObject.gameObject.SetActive(true); + gameObject.transform.SetParent(__instance._profileListRoot.transform); + gameObject.transform.localScale = new Vector3(1f, 1f, 1f); + Text[] componentsInChildren = gameObject.gameObject.GetComponentsInChildren(); + for (int k = 0; k < componentsInChildren.Length; k++) + { + __instance._fontController.AddTextElement(componentsInChildren[k], true, true, false); + } + + num++; + TwoButtonActionElement requiredComponent2 = gameObject.GetRequiredComponent(); + Selectable requiredComponent3 = requiredComponent2.GetRequiredComponent(); + __instance.SetUpProfileElementListeners(requiredComponent2); + requiredComponent2.SetLabelText(array[j].profileName); + Text component = requiredComponent2.GetButtonOne().GetComponent(); + if (component != null) + { + __instance._fontController.AddTextElement(component, true, true, false); + } + + component = requiredComponent2.GetButtonTwo().GetComponent(); + if (component != null) + { + __instance._fontController.AddTextElement(component, true, true, false); + } + + if (num == 1) + { + Navigation navigation = __instance._createProfileButton.navigation; + navigation.selectOnDown = gameObject.GetRequiredComponent(); + __instance._createProfileButton.navigation = navigation; + Navigation navigation2 = requiredComponent3.navigation; + navigation2.selectOnUp = __instance._createProfileButton; + requiredComponent3.navigation = navigation2; + } + else + { + Navigation navigation3 = requiredComponent3.navigation; + Navigation navigation4 = selectable.navigation; + navigation3.selectOnUp = selectable; + navigation3.selectOnDown = null; + navigation4.selectOnDown = requiredComponent3; + requiredComponent3.navigation = navigation3; + selectable.navigation = navigation4; + } + + __instance._listProfileElements.Add(gameObject); + selectable = requiredComponent3; + ProfileMenuManager.ProfileElementLookup profileElementLookup = new ProfileMenuManager.ProfileElementLookup(); + profileElementLookup.profileName = array[j].profileName; + profileElementLookup.lastModifiedTime = array[j].lastModifiedTime; + profileElementLookup.confirmSwitchAction = requiredComponent2.GetSubmitActionOne() as SubmitActionConfirm; + profileElementLookup.confirmDeleteAction = requiredComponent2.GetSubmitActionTwo() as SubmitActionConfirm; + __instance._listProfileUIElementLookup.Add(profileElementLookup); + } + } + + return false; + } + + [HarmonyPrefix] + [HarmonyPatch(nameof(ProfileMenuManager.SetCurrentProfileLabel))] + public static bool SetCurrentProfileLabel(ProfileMenuManager __instance) + { + __instance._currenProfileLabel.text = UITextLibrary.GetString(UITextType.MenuProfile) + + " " + + QSBStandaloneProfileManager.SharedInstance.currentProfile.profileName; + return false; + } +} diff --git a/QSB/SaveSync/Patches/TitleScreenManagerPatchesCommon.cs b/QSB/SaveSync/Patches/TitleScreenManagerPatchesCommon.cs new file mode 100644 index 00000000..afdfbb8b --- /dev/null +++ b/QSB/SaveSync/Patches/TitleScreenManagerPatchesCommon.cs @@ -0,0 +1,26 @@ +using HarmonyLib; +using QSB.Patches; + +namespace QSB.SaveSync.Patches; + +[HarmonyPatch(typeof(TitleScreenManager))] +internal class TitleScreenManagerPatchesCommon : QSBPatch +{ + public override QSBPatchTypes Type => QSBPatchTypes.OnModStart; + + [HarmonyPrefix] + [HarmonyPatch(nameof(TitleScreenManager.Awake))] + public static bool Awake(TitleScreenManager __instance) + { + __instance._profileManager = QSBCore.ProfileManager; + __instance._profileManager.PreInitialize(); + LoadManager.OnStartSceneLoad += __instance.OnStartSceneLoad; + LoadManager.OnCompleteSceneLoad += __instance.OnCompleteSceneLoad; + MenuStackManager.SharedInstance.OnMenuPush += __instance.OnMenuPush; + MenuStackManager.SharedInstance.OnMenuPop += __instance.OnMenuPop; + __instance._resumeGameTextSetter = __instance._resumeGameObject.GetComponentInChildren(); + __instance.InitializePopupPrompts(); + + return false; + } +} diff --git a/QSB/SaveSync/Patches/TitleScreenManagerPatchesGamepass.cs b/QSB/SaveSync/Patches/TitleScreenManagerPatchesGamepass.cs new file mode 100644 index 00000000..4f6fbe39 --- /dev/null +++ b/QSB/SaveSync/Patches/TitleScreenManagerPatchesGamepass.cs @@ -0,0 +1,63 @@ +using HarmonyLib; +using OWML.Utils; +using QSB.Patches; +using UnityEngine.UI; + +namespace QSB.SaveSync.Patches; + +[HarmonyPatch(typeof(TitleScreenManager))] +internal class TitleScreenManagerPatchesGamepass : QSBPatch +{ + public override QSBPatchTypes Type => QSBPatchTypes.OnModStart; + public override GameVendor PatchVendor => GameVendor.Gamepass; + + [HarmonyPrefix] + [HarmonyPatch("SetUserAccountDisplayInfo")] + public static bool SetUserAccountDisplayInfo(TitleScreenManager __instance) + { + var text = __instance.GetValue("_gamertagDisplay"); + text.text = ""; // no idea why, mobius be like + text.text = QSBMSStoreProfileManager.SharedInstance.userDisplayName; + return false; + } + + [HarmonyPrefix] + [HarmonyPatch(nameof(TitleScreenManager.InitializeProfileManagerCallbacks))] + public static bool InitializeProfileManagerCallbacks(TitleScreenManager __instance) + { + QSBMSStoreProfileManager.SharedInstance.OnBrokenDataExists += __instance.OnBrokenDataExists; + + __instance._profileManager.OnProfileSignInStart += __instance.OnProfileSignInStart; + __instance._profileManager.OnProfileSignInComplete += __instance.OnProfileSignInComplete; + __instance._profileManager.OnProfileSignOutStart += __instance.OnProfileSignOutStart; + __instance._profileManager.OnProfileSignOutComplete += __instance.OnProfileSignOutComplete; + __instance._profileManager.OnProfileReadDone += __instance.OnProfileManagerReadDone; + __instance._profileManager.Initialize(); + + return false; + } + + [HarmonyPrefix] + [HarmonyPatch(nameof(TitleScreenManager.OnDestroy))] + public static bool OnDestroy(TitleScreenManager __instance) + { + QSBMSStoreProfileManager.SharedInstance.OnBrokenDataExists -= __instance.OnBrokenDataExists; + + __instance._profileManager.OnProfileSignInStart -= __instance.OnProfileSignInStart; + __instance._profileManager.OnProfileSignInComplete -= __instance.OnProfileSignInComplete; + __instance._profileManager.OnProfileSignOutStart -= __instance.OnProfileSignOutStart; + __instance._profileManager.OnProfileSignOutComplete -= __instance.OnProfileSignOutComplete; + __instance._profileManager.OnProfileReadDone -= __instance.OnProfileManagerReadDone; + LoadManager.OnStartSceneLoad -= __instance.OnStartSceneLoad; + LoadManager.OnCompleteSceneLoad -= __instance.OnCompleteSceneLoad; + TextTranslation.Get().OnLanguageChanged -= __instance.OnLanguageChanged; + __instance._newGameAction.OnSubmitAction -= __instance.OnNewGameSubmit; + __instance._newGameAction.OnPostSetupPopup -= __instance.OnNewGameSetupPopup; + __instance._resetGameAction.OnSubmitAction -= __instance.OnResetGameSubmit; + __instance._accountPickerSubmitAction.OnAccountPickerSubmitEvent -= __instance.OnAccountPickerSubmitEvent; + MenuStackManager.SharedInstance.OnMenuPush -= __instance.OnMenuPush; + MenuStackManager.SharedInstance.OnMenuPop -= __instance.OnMenuPop; + + return false; + } +} diff --git a/QSB/SaveSync/Patches/TitleScreenManagerPatchesStandalone.cs b/QSB/SaveSync/Patches/TitleScreenManagerPatchesStandalone.cs new file mode 100644 index 00000000..3bae3f7a --- /dev/null +++ b/QSB/SaveSync/Patches/TitleScreenManagerPatchesStandalone.cs @@ -0,0 +1,89 @@ +using HarmonyLib; +using QSB.Patches; + +namespace QSB.SaveSync.Patches; + +[HarmonyPatch(typeof(TitleScreenManager))] +internal class TitleScreenManagerPatchesStandalone : QSBPatch +{ + public override QSBPatchTypes Type => QSBPatchTypes.OnModStart; + public override GameVendor PatchVendor => GameVendor.Epic | GameVendor.Steam; + + [HarmonyPrefix] + [HarmonyPatch(nameof(TitleScreenManager.OnBrokenDataExists))] + public static bool OnBrokenDataExists(TitleScreenManager __instance) + { + __instance._titleMenuRaycastBlocker.blocksRaycasts = false; + __instance._inputModule.EnableInputs(); + __instance._waitingOnBrokenDataResponse = true; + var flag = QSBStandaloneProfileManager.SharedInstance.BackupExistsForBrokenData(); + var text = UITextLibrary.GetString(UITextType.SaveRestore_CorruptedMsg); + if (flag) + { + text = text + " " + UITextLibrary.GetString(UITextType.SaveRestore_LoadPreviousMsg); + } + + __instance._okCancelPopup.ResetPopup(); + __instance._okCancelPopup.SetUpPopup(text, InputLibrary.confirm, InputLibrary.cancel, __instance._confirmActionPrompt, __instance._cancelActionPrompt, true, flag); + __instance._okCancelPopup.OnPopupConfirm += __instance.OnUserConfirmRestoreData; + __instance._okCancelPopup.OnPopupCancel += __instance.OnUserCancelRestoreData; + __instance._okCancelPopup.EnableMenu(true); + return false; + } + + [HarmonyPrefix] + [HarmonyPatch(nameof(TitleScreenManager.OnUserConfirmRestoreData))] + public static bool OnUserConfirmRestoreData(TitleScreenManager __instance) + { + __instance._waitingOnBrokenDataResponse = false; + QSBStandaloneProfileManager.SharedInstance.RestoreCurrentProfileBackup(); + __instance.OnProfileManagerReadDone(); + __instance._okCancelPopup.OnPopupConfirm -= __instance.OnUserConfirmRestoreData; + __instance._okCancelPopup.OnPopupCancel -= __instance.OnUserCancelRestoreData; + return false; + } + + [HarmonyPrefix] + [HarmonyPatch(nameof(TitleScreenManager.InitializeProfileManagerCallbacks))] + public static bool InitializeProfileManagerCallbacks(TitleScreenManager __instance) + { + QSBStandaloneProfileManager.SharedInstance.OnNoProfilesExist += __instance.OnNoStandaloneProfilesExist; + QSBStandaloneProfileManager.SharedInstance.OnUpdatePlayerProfiles += __instance.OnUpdatePlayerProfiles; + QSBStandaloneProfileManager.SharedInstance.OnBrokenDataExists += __instance.OnBrokenDataExists; + + __instance._profileManager.OnProfileSignInStart += __instance.OnProfileSignInStart; + __instance._profileManager.OnProfileSignInComplete += __instance.OnProfileSignInComplete; + __instance._profileManager.OnProfileSignOutStart += __instance.OnProfileSignOutStart; + __instance._profileManager.OnProfileSignOutComplete += __instance.OnProfileSignOutComplete; + __instance._profileManager.OnProfileReadDone += __instance.OnProfileManagerReadDone; + __instance._profileManager.Initialize(); + + return false; + } + + [HarmonyPrefix] + [HarmonyPatch(nameof(TitleScreenManager.OnDestroy))] + public static bool OnDestroy(TitleScreenManager __instance) + { + QSBStandaloneProfileManager.SharedInstance.OnNoProfilesExist -= __instance.OnNoStandaloneProfilesExist; + QSBStandaloneProfileManager.SharedInstance.OnUpdatePlayerProfiles -= __instance.OnUpdatePlayerProfiles; + QSBStandaloneProfileManager.SharedInstance.OnBrokenDataExists -= __instance.OnBrokenDataExists; + + __instance._profileManager.OnProfileSignInStart -= __instance.OnProfileSignInStart; + __instance._profileManager.OnProfileSignInComplete -= __instance.OnProfileSignInComplete; + __instance._profileManager.OnProfileSignOutStart -= __instance.OnProfileSignOutStart; + __instance._profileManager.OnProfileSignOutComplete -= __instance.OnProfileSignOutComplete; + __instance._profileManager.OnProfileReadDone -= __instance.OnProfileManagerReadDone; + LoadManager.OnStartSceneLoad -= __instance.OnStartSceneLoad; + LoadManager.OnCompleteSceneLoad -= __instance.OnCompleteSceneLoad; + TextTranslation.Get().OnLanguageChanged -= __instance.OnLanguageChanged; + __instance._newGameAction.OnSubmitAction -= __instance.OnNewGameSubmit; + __instance._newGameAction.OnPostSetupPopup -= __instance.OnNewGameSetupPopup; + __instance._resetGameAction.OnSubmitAction -= __instance.OnResetGameSubmit; + __instance._accountPickerSubmitAction.OnAccountPickerSubmitEvent -= __instance.OnAccountPickerSubmitEvent; + MenuStackManager.SharedInstance.OnMenuPush -= __instance.OnMenuPush; + MenuStackManager.SharedInstance.OnMenuPop -= __instance.OnMenuPop; + + return false; + } +} diff --git a/QSB/SaveSync/QSBMSStoreProfileManager.cs b/QSB/SaveSync/QSBMSStoreProfileManager.cs new file mode 100644 index 00000000..3780b4d7 --- /dev/null +++ b/QSB/SaveSync/QSBMSStoreProfileManager.cs @@ -0,0 +1,429 @@ +using Microsoft.Xbox; +using Newtonsoft.Json; +using System; +using System.IO; +using System.Runtime.Serialization; +using System.Xml.Serialization; +using UnityEngine; +using UnityEngine.InputSystem; + +namespace QSB.SaveSync; + +internal class QSBMSStoreProfileManager : IProfileManager +{ + private const string OW_SAVE_CONTAINER_NAME = "GameSave"; + private const string OW_GAME_SAVE_BLOB_NAME = "Outer Wilds Converted"; + private const string OW_GAME_SETTINGS_BLOB_NAME = "PCGameSettings"; + + private static QSBMSStoreProfileManager _sharedInstance; + private QSBX1SaveData _saveData; + private const string c_containerName = "OuterWildsConnectedStorage"; + private GameSave _pendingGameSave; + private SettingsSave _pendingSettingsSave; + private GraphicSettings _pendingGfxSettingsSave; + private string _pendingInputActionsSave = ""; + private JsonSerializer _jsonSerializer; + private bool _initialized; + private int _fileOpsBusyLocks; + private bool _preInitialized; + private bool _isLoadingGameBlob; + private bool _isLoadingSettingsBlob; + + public static QSBMSStoreProfileManager SharedInstance + { + get + { + if (_sharedInstance == null) + { + _sharedInstance = new QSBMSStoreProfileManager(); + } + + return _sharedInstance; + } + } + + public GameSave currentProfileGameSave => _saveData.gameSave; + public GameSave currentProfileMultiplayerGameSave => _saveData.gameMultSave; + public SettingsSave currentProfileGameSettings => _saveData.settings; + public GraphicSettings currentProfileGraphicsSettings => _saveData.gfxSettings; + public string currentProfileInputJSON => _saveData.inputActionsJson; + public bool isInitialized { get; } + public bool isBusyWithFileOps => _fileOpsBusyLocks > 0; + public bool hasPendingSaveOperation => _pendingGameSave != null || _pendingSettingsSave != null || _pendingGfxSettingsSave != null || _pendingInputActionsSave != null; + public bool saveSystemAvailable { get; private set; } + public string userDisplayName => Gdk.Helpers.currentGamertag; + + public delegate void BrokenDataExistsEvent(); + + public event BrokenDataExistsEvent OnBrokenDataExists; + public event ProfileDataSavedEvent OnProfileDataSaved; + public event ProfileReadDoneEvent OnProfileReadDone; + public event ProfileSignInCompleteEvent OnProfileSignInComplete; + public event ProfileSignInStartEvent OnProfileSignInStart; + public event ProfileSignOutCompleteEvent OnProfileSignOutComplete; + public event ProfileSignOutStartEvent OnProfileSignOutStart; + + public void Initialize() + { + if (!_initialized) + { + Gdk.Helpers.SignIn(); + SpinnerUI.Show(); + Debug.Log("MSStoreProfileManager.Initialize"); + Gdk.Helpers.OnGameSaveSucceeded += OnGameSaveComplete; + Gdk.Helpers.OnGameSaveFailed += OnGameSaveFailed; + Gdk.Helpers.OnGameSaveLoaded += OnGameSaveLoaded; + Gdk.Helpers.OnGameSaveLoadFailed += OnGameSaveLoadFailed; + Achievements.Init(); + var serializationBinder = new VersionDeserializationBinder(); + _jsonSerializer = new JsonSerializer + { + SerializationBinder = serializationBinder + }; + _initialized = true; + return; + } + + OnProfileSignInComplete?.Invoke(ProfileManagerSignInResult.COMPLETE); + + var onProfileReadDone = OnProfileReadDone; + if (onProfileReadDone == null) + { + return; + } + + onProfileReadDone(); + } + + public void PreInitialize() + { + if (_preInitialized) + { + return; + } + + _fileOpsBusyLocks = 0; + _pendingGameSave = null; + _pendingSettingsSave = null; + _pendingGfxSettingsSave = null; + _pendingInputActionsSave = null; + _preInitialized = true; + } + + public void InvokeProfileSignInComplete() + { + var onProfileSignInComplete = OnProfileSignInComplete; + if (onProfileSignInComplete == null) + { + return; + } + + onProfileSignInComplete(ProfileManagerSignInResult.COMPLETE); + } + + public void InvokeSaveSetupComplete() + { + saveSystemAvailable = true; + _isLoadingGameBlob = true; + _saveData = new QSBX1SaveData(); + LoadGame(OW_GAME_SAVE_BLOB_NAME); + } + + public void InitializeForEditor() + { + } + + public void SaveGame(GameSave gameSave, SettingsSave settSave, GraphicSettings gfxSettings, string inputJSON) + { + Debug.Log("MSStoreProfileManager.SaveGame"); + if (isBusyWithFileOps || LoadManager.IsBusy()) + { + _pendingGameSave = gameSave; + _pendingSettingsSave = settSave; + _pendingGfxSettingsSave = gfxSettings; + _pendingInputActionsSave = inputJSON; + return; + } + + var gameSaveData = new QSBX1SaveData(); + var settingsSaveData = new QSBX1SaveData(); + var saveGameSave = false; + if (gameSave != null) + { + saveGameSave = true; + + if (QSBCore.IsInMultiplayer) + { + _saveData.gameMultSave = gameSave; + gameSaveData.gameMultSave = gameSave; + } + else + { + _saveData.gameSave = gameSave; + gameSaveData.gameSave = gameSave; + } + } + + var saveGameSettings = false; + if (settSave != null) + { + saveGameSettings = true; + _saveData.settings = settSave; + settingsSaveData.settings = settSave; + } + else + { + settingsSaveData.settings = _saveData.settings; + } + + if (gfxSettings != null) + { + saveGameSettings = true; + _saveData.gfxSettings = gfxSettings; + settingsSaveData.gfxSettings = gfxSettings; + } + else + { + settingsSaveData.gfxSettings = _saveData.gfxSettings; + } + + if (!string.IsNullOrEmpty(inputJSON)) + { + saveGameSettings = true; + _saveData.inputActionsJson = inputJSON; + settingsSaveData.inputActionsJson = inputJSON; + } + else if (!string.IsNullOrEmpty(_saveData.inputActionsJson)) + { + settingsSaveData.inputActionsJson = _saveData.inputActionsJson; + } + else + { + settingsSaveData.inputActionsJson = ((InputManager)OWInput.SharedInputManager).commandManager.DefaultInputActions.ToJson(); + } + + if (saveGameSave) + { + WriteSaveToStorage(gameSaveData, OW_GAME_SAVE_BLOB_NAME); + } + + if (saveGameSettings) + { + WriteSaveToStorage(settingsSaveData, OW_GAME_SETTINGS_BLOB_NAME); + } + } + + private void LoadGame(string blobName) + { + _fileOpsBusyLocks++; + Gdk.Helpers.LoadSaveData(blobName); + } + + private void WriteSaveToStorage(QSBX1SaveData saveData, string blobName) + { + Debug.Log("Saving to storage: " + blobName); + _fileOpsBusyLocks++; + var memoryStream = new MemoryStream(); + using (JsonWriter jsonWriter = new JsonTextWriter(new StreamWriter(memoryStream))) + { + _jsonSerializer.Serialize(jsonWriter, saveData); + } + + var buffer = memoryStream.GetBuffer(); + Gdk.Helpers.Save(buffer, blobName); + } + + public void PerformPendingSaveOperation() + { + if (!isBusyWithFileOps && !LoadManager.IsBusy()) + { + SaveGame(_pendingGameSave, _pendingSettingsSave, _pendingGfxSettingsSave, _pendingInputActionsSave); + _pendingGameSave = null; + _pendingSettingsSave = null; + _pendingGfxSettingsSave = null; + _pendingInputActionsSave = null; + } + } + + private void OnGameSaveComplete(object sender, string blobName) + { + _fileOpsBusyLocks--; + Debug.Log("[GDK] save to blob " + blobName + " complete"); + } + + private void OnGameSaveFailed(object sender, string blobName) + { + _fileOpsBusyLocks--; + Debug.Log("[GDK] save to blob " + blobName + " failed"); + } + + private void OnGameSaveLoaded(object sender, string blobName, GameSaveLoadedArgs saveData) + { + _fileOpsBusyLocks--; + Debug.Log("[GDK] save file load complete! blob name: " + blobName); + var memoryStream = new MemoryStream(saveData.Data); + memoryStream.Seek(0L, SeekOrigin.Begin); + using (var jsonTextReader = new JsonTextReader(new StreamReader(memoryStream))) + { + var x1SaveData = _jsonSerializer.Deserialize(jsonTextReader); + if (_isLoadingGameBlob) + { + if (x1SaveData != null) + { + if (x1SaveData.gameSave == null) + { + Debug.Log("[GDK] tempSaveData.gameSave is null (oh no)"); + } + + _saveData.gameSave = x1SaveData.gameSave ?? new GameSave(); + } + else + { + Debug.Log("[GDK] tempSaveData is null (oh no)"); + _saveData.gameSave = new GameSave(); + } + } + else + { + if (x1SaveData != null) + { + _saveData.gfxSettings = x1SaveData.gfxSettings ?? new GraphicSettings(true); + _saveData.settings = x1SaveData.settings ?? new SettingsSave(); + _saveData.inputActionsJson = x1SaveData.inputActionsJson ?? ((InputManager)OWInput.SharedInputManager).commandManager.DefaultInputActions.ToJson(); + } + else + { + _saveData.gfxSettings = new GraphicSettings(true); + _saveData.settings = new SettingsSave(); + _saveData.inputActionsJson = ((InputManager)OWInput.SharedInputManager).commandManager.DefaultInputActions.ToJson(); + } + + Debug.Log(string.Format("after settings load, _saveData.gameSave is null: {0}", _saveData.gameSave == null)); + Debug.Log(string.Format("_saveData loopCount: {0}", _saveData.gameSave.loopCount)); + } + } + + if (_isLoadingGameBlob) + { + _isLoadingGameBlob = false; + LoadGame(OW_GAME_SETTINGS_BLOB_NAME); + _isLoadingSettingsBlob = true; + return; + } + + if (_isLoadingSettingsBlob) + { + _isLoadingSettingsBlob = false; + var onProfileReadDone = OnProfileReadDone; + if (onProfileReadDone == null) + { + return; + } + + onProfileReadDone(); + } + } + + private void OnGameSaveLoadFailed(object sender, string blobName) + { + _fileOpsBusyLocks--; + if (_isLoadingGameBlob) + { + _saveData.gameSave = new GameSave(); + SaveGame(_saveData.gameSave, null, null, null); + _isLoadingGameBlob = false; + LoadGame(OW_GAME_SETTINGS_BLOB_NAME); + _isLoadingSettingsBlob = true; + return; + } + + if (_isLoadingSettingsBlob) + { + _saveData.settings = new SettingsSave(); + _saveData.gfxSettings = new GraphicSettings(true); + _saveData.inputActionsJson = ((InputManager)OWInput.SharedInputManager).commandManager.DefaultInputActions.ToJson(); + SaveGame(null, _saveData.settings, _saveData.gfxSettings, _saveData.inputActionsJson); + _isLoadingSettingsBlob = false; + var onProfileReadDone = OnProfileReadDone; + if (onProfileReadDone == null) + { + return; + } + + onProfileReadDone(); + } + } + + [Serializable] + public class QSBX1SaveData + { + [XmlElement("gameSave")] + public GameSave gameSave; + + [XmlElement("gameMultSave")] + [OptionalField(VersionAdded = 5)] + public GameSave gameMultSave; + + [XmlElement("settings")] + public SettingsSave settings; + + [XmlElement("gfxSettings")] + [OptionalField(VersionAdded = 2)] + public GraphicSettings gfxSettings; + + [OptionalField(VersionAdded = 3)] + [NonSerialized] + public InputRebindableData bindingSettings; + + [OptionalField(VersionAdded = 4)] + public string inputActionsPacked; + + private InputActionAsset _inputActionsSave; + + [JsonIgnore] + public string inputActionsJson + { + get => inputActionsPacked; + set + { + inputActionsPacked = value; + if (!string.IsNullOrEmpty(inputActionsPacked)) + { + _inputActionsSave = InputActionAsset.FromJson(inputActionsPacked); + return; + } + + _inputActionsSave = ((InputManager)OWInput.SharedInputManager).commandManager.DefaultInputActions; + } + } + + [JsonIgnore] + public InputActionAsset inputActionsSave + { + get + { + if (_inputActionsSave == null && !string.IsNullOrEmpty(inputActionsPacked)) + { + try + { + _inputActionsSave = InputActionAsset.FromJson(inputActionsPacked); + } + catch (Exception) + { + _inputActionsSave = null; + } + } + + return _inputActionsSave; + } + } + + [OnDeserializing] + private void SetDefaultValuesOnDeserializing(StreamingContext context) + { + gfxSettings = null; + bindingSettings = null; + inputActionsPacked = null; + } + } +} diff --git a/QSB/SaveSync/QSBStandaloneProfileManager.cs b/QSB/SaveSync/QSBStandaloneProfileManager.cs new file mode 100644 index 00000000..5c91f1b0 --- /dev/null +++ b/QSB/SaveSync/QSBStandaloneProfileManager.cs @@ -0,0 +1,1193 @@ +using Newtonsoft.Json; +using QSB.Utility; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.Serialization; +using System.Runtime.Serialization.Formatters.Binary; +using UnityEngine; + +namespace QSB.SaveSync; + +internal class QSBStandaloneProfileManager : IProfileManager +{ + private static QSBStandaloneProfileManager s_instance; + + private const string _saveDirectory = "/SteamSaves"; + private const string _backupDirectory = "/Backup"; + private const string _tempDirectory = "/Temp"; + private const string _gameSaveFilename = "data.owsave"; + private const string _gameSaveMultFilename = "data_mult.owsave"; + private const string _gameSettingsFilename = "player.owsett"; + private const string _gfxSettingsFilename = "graphics.owsett"; + private const string _legacyInputBindingSettingsFilename = "input.owsett"; + private const string _inputActionsSettingsFilename = "input_new.owsett"; + private const int _profileNameCharLimit = 16; + + private string _profilesPath; + private string _profileTempPath; + private string _profileBackupPath; + private int _fileOpsBusyLocks; + private GameSave _pendingGameSave; + private SettingsSave _pendingSettingsSave; + private GraphicSettings _pendingGfxSettingsSave; + private string _pendingInputJSONSave = ""; + private BinaryFormatter _binaryFormatter; + private JsonSerializer _jsonSerializer; + + public static QSBStandaloneProfileManager SharedInstance + { + get + { + if (s_instance == null) + { + s_instance = new QSBStandaloneProfileManager(); + } + + return s_instance; + } + } + + public GameSave currentProfileGameSave => currentProfile?.gameSave; + + public SettingsSave currentProfileGameSettings => currentProfile?.settingsSave; + + public GraphicSettings currentProfileGraphicsSettings => currentProfile?.graphicsSettings; + + public string currentProfileInputJSON => currentProfile?.inputJSON; + + public QSBProfileData currentProfile { get; private set; } + + public QSBProfileData mostRecentProfile + => profiles.OrderByDescending(profile => profile.lastModifiedTime).FirstOrDefault(); + + public int profileNameCharacterLimit => _profileNameCharLimit; + + public List profiles { get; private set; } + + public int numberOfProfiles => profiles.Count; + + public bool isInitialized => currentProfileGameSave != null; + + public bool isBusyWithFileOps => _fileOpsBusyLocks > 0; + + public bool hasPendingSaveOperation => _pendingGameSave != null + || _pendingSettingsSave != null + || _pendingGfxSettingsSave != null + || _pendingInputJSONSave != ""; + + + public int profileCharacterLimit => _profileNameCharLimit; + + public delegate void NoProfilesExistEvent(); + public delegate void BrokenDataExistsEvent(); + public delegate void BackupDataRestoredEvent(); + public delegate void UpdatePlayerProfilesEvent(); + + public event NoProfilesExistEvent OnNoProfilesExist; + public event BrokenDataExistsEvent OnBrokenDataExists; + public event BackupDataRestoredEvent OnBackupDataRestored; + public event UpdatePlayerProfilesEvent OnUpdatePlayerProfiles; + public event ProfileSignInCompleteEvent OnProfileSignInComplete; + public event ProfileReadDoneEvent OnProfileReadDone; + public event ProfileDataSavedEvent OnProfileDataSaved; + public event ProfileSignOutCompleteEvent OnProfileSignOutComplete; + public event ProfileSignInStartEvent OnProfileSignInStart; + public event ProfileSignOutStartEvent OnProfileSignOutStart; + public event ControllerDisconnectedEvent OnControllerDisconnected; + public event ControllerReconnectedEvent OnControllerReconnected; + + public void PreInitialize() + { + _fileOpsBusyLocks = 0; + _pendingGameSave = null; + _pendingSettingsSave = null; + _pendingGfxSettingsSave = null; + _pendingInputJSONSave = ""; + } + + public void Initialize() + { + _profilesPath = Application.persistentDataPath + _saveDirectory; + _profileBackupPath = Application.persistentDataPath + _backupDirectory; + _profileTempPath = Application.persistentDataPath + _tempDirectory; + profiles = new List(); + var versionDeserializationBinder = new VersionDeserializationBinder(); + _jsonSerializer = new JsonSerializer + { + SerializationBinder = versionDeserializationBinder + }; + _binaryFormatter = new BinaryFormatter + { + Binder = versionDeserializationBinder + }; + Achievements.Init(); + InitializeProfileData(); + } + + public void InitializeForEditor() + { + _profilesPath = Application.persistentDataPath + _saveDirectory; + _profileBackupPath = Application.persistentDataPath + _backupDirectory; + _profileTempPath = Application.persistentDataPath + _tempDirectory; + profiles = new List(); + var versionDeserializationBinder = new VersionDeserializationBinder(); + _jsonSerializer = new JsonSerializer + { + SerializationBinder = versionDeserializationBinder + }; + _binaryFormatter = new BinaryFormatter + { + Binder = versionDeserializationBinder + }; + MarkBusyWithFileOps(true); + profiles.Clear(); + LoadProfiles(); + LoadSaveFilesFromProfiles(); + var flag = false; + for (var i = 0; i < profiles.Count; i++) + { + if (profiles[i].profileName == "Debug") + { + currentProfile = profiles[i]; + flag = true; + break; + } + } + + if (!flag) + { + TryCreateProfile("Debug"); + } + + MarkBusyWithFileOps(false); + PlayerData.Init(currentProfileGameSave, currentProfileGameSettings, currentProfileGraphicsSettings, currentProfileInputJSON); + } + + private void MarkBusyWithFileOps(bool isBusy) + { + if (isBusy) + { + _fileOpsBusyLocks++; + return; + } + + if (_fileOpsBusyLocks <= 0) + { + Debug.LogWarning("No File I/O lock to remove!"); + return; + } + + _fileOpsBusyLocks--; + } + + public void PerformPendingSaveOperation() + { + if (!isBusyWithFileOps && !LoadManager.IsBusy()) + { + TrySaveProfile(currentProfile, _pendingGameSave, _pendingSettingsSave, _pendingGfxSettingsSave, _pendingInputJSONSave); + _pendingGameSave = null; + _pendingSettingsSave = null; + _pendingGfxSettingsSave = null; + _pendingInputJSONSave = ""; + } + } + + public void SaveGame(GameSave gameSave, SettingsSave settSave, GraphicSettings graphicSettings, string inputBindings) + { + if (isBusyWithFileOps || LoadManager.IsBusy()) + { + _pendingGameSave = gameSave; + _pendingSettingsSave = settSave; + _pendingGfxSettingsSave = graphicSettings; + _pendingInputJSONSave = inputBindings; + return; + } + + TrySaveProfile(currentProfile, gameSave, settSave, graphicSettings, inputBindings); + } + + private void InitializeProfileData() + { + LoadProfiles(); + currentProfile = mostRecentProfile; + if (currentProfile != null) + { + LoadSaveFilesFromProfiles(); + return; + } + + var onNoProfilesExist = OnNoProfilesExist; + if (onNoProfilesExist == null) + { + return; + } + + onNoProfilesExist(); + } + + private void LoadSaveFilesFromProfiles() + { + MarkBusyWithFileOps(isBusy: true); + foreach (var profile in profiles) + { + var path = _profilesPath + "/" + profile.profileName; + GameSave saveData = null; + GameSave multSaveData = null; + SettingsSave settingsData = null; + GraphicSettings graphicsData = null; + var inputJSON = ""; + if (Directory.Exists(path)) + { + Stream stream = null; + var directoryInfo = new DirectoryInfo(path); + profile.brokenSaveData = TryLoadSaveData(ref stream, _gameSaveFilename, directoryInfo, out saveData); + profile.brokenMultSaveData = TryLoadSaveData(ref stream, _gameSaveMultFilename, directoryInfo, out multSaveData); + profile.brokenSettingsData = TryLoadSaveData(ref stream, _gameSettingsFilename, directoryInfo, out settingsData); + profile.brokenGfxSettingsData = TryLoadSaveData(ref stream, _gfxSettingsFilename, directoryInfo, out graphicsData); + profile.brokenRebindingData = TryLoadInputBindingsSave(ref stream, directoryInfo, out inputJSON); + } + + var profilePath = _profileBackupPath + "/" + profile.profileName; + var savePath = profilePath + "/" + _gameSaveFilename; + var multSavePath = profilePath + "/" + _gameSaveMultFilename; + var settingsPath = profilePath + "/" + _gameSettingsFilename; + var graphicsPath = profilePath + "/" + _gfxSettingsFilename; + var inputsPath = profilePath + "/" + _inputActionsSettingsFilename; + + if (saveData == null) + { + profile.brokenSaveData = File.Exists(savePath); + saveData = new GameSave(); + Debug.LogError("Could not find game save for " + profile.profileName); + } + + if (multSaveData == null) + { + profile.brokenMultSaveData = File.Exists(multSavePath); + multSaveData = new GameSave(); + Debug.LogError("Could not find multiplayer game save for " + profile.profileName); + } + + if (settingsData == null) + { + profile.brokenSettingsData = File.Exists(settingsPath); + settingsData = new SettingsSave(); + Debug.LogError("Could not find game settings for " + profile.profileName); + } + + if (graphicsData == null) + { + profile.brokenGfxSettingsData = File.Exists(graphicsPath); + graphicsData = new GraphicSettings(init: true); + Debug.LogError("Could not find graphics settings for " + profile.profileName); + } + + if (inputJSON == "") + { + profile.brokenRebindingData = File.Exists(inputsPath); + inputJSON = ((InputManager)OWInput.SharedInputManager).commandManager.DefaultInputActions.ToJson(); + Debug.LogError("Could not find input action settings for " + profile.profileName); + } + + profile.gameSave = saveData; + profile.multiplayerGameSave = multSaveData; + profile.settingsSave = settingsData; + profile.graphicsSettings = graphicsData; + profile.inputJSON = inputJSON; + } + + MarkBusyWithFileOps(isBusy: false); + if (CurrentProfileHasBrokenData()) + { + OnBrokenDataExists?.Invoke(); + } + + OnProfileReadDone?.Invoke(); + } + + private bool TryLoadSaveData(ref Stream stream, string fileName, DirectoryInfo directoryInfo, out T saveData) + { + saveData = default; + var flag = true; + var files = directoryInfo.GetFiles(fileName); + if (files.Length != 0) + { + stream = null; + if (TryOpenFile(files[0].FullName, ref stream)) + { + var jsonTextReader = new JsonTextReader(new StreamReader(stream)); + flag = !TryDeserializeJson(jsonTextReader, out saveData); + if (flag) + { + stream.Position = 0L; + flag = !TryDeserializeBinary(stream, out saveData); + } + + jsonTextReader.Close(); + } + } + + return flag; + } + + private bool TryLoadInputBindingsSave(ref Stream stream, DirectoryInfo directoryInfo, out string inputJSON) + { + inputJSON = null; + var result = true; + var files = directoryInfo.GetFiles(_inputActionsSettingsFilename); + if (files.Length != 0) + { + stream = null; + if (TryOpenFile(files[0].FullName, ref stream)) + { + result = !TryDeserializeJsonAsInputActionsData(stream, out inputJSON); + } + + var stream2 = stream; + if (stream2 != null) + { + stream2.Close(); + } + } + + return result; + } + + private bool TryOpenFile(string fullPath, ref Stream dataStream) + { + bool result; + try + { + dataStream = File.Open(fullPath, FileMode.Open); + result = true; + } + catch (Exception ex) + { + Debug.LogError("[" + ex.Message + "] Failed loading opening file " + fullPath); + result = false; + } + + return result; + } + + private bool TryDeserializeBinary(Stream dataStream, out T saveData) + { + bool result; + try + { + saveData = default; + saveData = (T)_binaryFormatter.Deserialize(dataStream); + Debug.Log("Successfully read " + typeof(T).Name + " save data as binary"); + result = true; + } + catch (Exception ex) + { + saveData = default; + Debug.LogError(string.Concat(new string[] + { + "[", + ex.Message, + "] Deserialization error for binary ", + typeof(T).Name, + " save data" + })); + result = false; + } + + return result; + } + + private bool TryDeserializeJson(JsonTextReader jsonReader, out T rebindingData) + { + bool result; + try + { + rebindingData = _jsonSerializer.Deserialize(jsonReader); + result = true; + } + catch (Exception) + { + rebindingData = default; + Debug.LogWarning("Could not read " + typeof(T).Name + " save data as JSON, it might be in binary so giving that a try."); + result = false; + } + + return result; + } + + private bool TryDeserializeJsonAsInputActionsData(Stream dataStream, out string inputJSON) + { + bool result; + try + { + using var streamReader = new StreamReader(dataStream); + var text = streamReader.ReadToEnd(); + inputJSON = text; + Debug.Log("Successfully read Input Bindings save data as JSON"); + result = true; + } + catch (Exception ex) + { + inputJSON = null; + Debug.LogError("[" + ex.Message + "] Deserialization error for Input Actions Save"); + result = false; + } + + return result; + } + + public bool CurrentProfileHasBrokenData() + { + if (currentProfile == null) + { + Debug.LogError("QSBStandaloneProfileManager.CurrentProfileHasBrokenData We should never get here outside of the Unity Editor"); + return false; + } + + return currentProfile.brokenSaveData || currentProfile.brokenMultSaveData || currentProfile.brokenSettingsData || currentProfile.brokenGfxSettingsData || currentProfile.brokenRebindingData; + } + + public bool BackupExistsForBrokenData() + { + var text = _profileBackupPath + "/" + currentProfile.profileName; + var savePath = text + "/" + _gameSaveFilename; + var multSavePath = text + "/" + _gameSaveMultFilename; + var settingsPath = text + "/" + _gameSettingsFilename; + var graphicsPath = text + "/" + _gfxSettingsFilename; + var inputsPath = text + "/" + _inputActionsSettingsFilename; + + return (currentProfile.brokenSaveData && File.Exists(savePath)) + || (currentProfile.brokenMultSaveData && File.Exists(multSavePath)) + || (currentProfile.brokenSettingsData && File.Exists(settingsPath)) + || (currentProfile.brokenGfxSettingsData && File.Exists(graphicsPath)) + || (currentProfile.brokenRebindingData && File.Exists(inputsPath)); + } + + private void LoadProfiles() + { + MarkBusyWithFileOps(true); + profiles.Clear(); + if (Directory.Exists(_profilesPath)) + { + QSBProfileData profileData = null; + Stream stream = null; + var files = new DirectoryInfo(_profilesPath).GetFiles("*.owprofile"); + foreach (var fileInfo in files) + { + DebugLog.DebugWrite(fileInfo.Name); + try + { + stream = null; + stream = File.Open(fileInfo.FullName, FileMode.Open); + var jsonTextReader = new JsonTextReader(new StreamReader(stream)); + try + { + profileData = _jsonSerializer.Deserialize(jsonTextReader); + } + catch + { + stream.Position = 0L; + profileData = (QSBProfileData)_binaryFormatter.Deserialize(stream); + } + finally + { + jsonTextReader.Close(); + } + + if (profileData == null) + { + DebugLog.DebugWrite("Profile at " + fileInfo.FullName + " null. Skipping."); + } + else + { + profiles.Add(profileData); + } + } + catch (Exception ex) + { + DebugLog.ToConsole("[" + ex.Message + "] Failed loading profile at " + fileInfo.Name, OWML.Common.MessageType.Error); + stream?.Close(); + } + } + } + else + { + DebugLog.DebugWrite($"{_profilesPath} does not exist"); + } + + MarkBusyWithFileOps(false); + } + + public void RestoreCurrentProfileBackup() + { + MarkBusyWithFileOps(isBusy: true); + var profilePath = _profilesPath + "/" + currentProfile.profileName; + var savePath = profilePath + "/" + _gameSaveFilename; + var multSavePath = profilePath + "/" + _gameSaveMultFilename; + var settingsPath = profilePath + "/" + _gameSettingsFilename; + var graphicsPath = profilePath + "/" + _gfxSettingsFilename; + var inputsPath = profilePath + "/" + _inputActionsSettingsFilename; + + var profileBackupPath = _profileBackupPath + "/" + currentProfile.profileName; + var saveBackupPath = profileBackupPath + "/" + _gameSaveFilename; + var multSaveBackupPath = profileBackupPath + "/" + _gameSaveMultFilename; + var settingsBackupPath = profileBackupPath + "/" + _gameSettingsFilename; + var graphicsBackupPath = profileBackupPath + "/" + _gfxSettingsFilename; + var inputsBackupPath = profileBackupPath + "/" + _inputActionsSettingsFilename; + + Stream stream = null; + try + { + if (!Directory.Exists(_profilesPath)) + { + Directory.CreateDirectory(_profilesPath); + } + + if (!Directory.Exists(_profileTempPath)) + { + Directory.CreateDirectory(_profileTempPath); + } + + if (!Directory.Exists(_profileBackupPath)) + { + Directory.CreateDirectory(_profileBackupPath); + } + + if (!Directory.Exists(profilePath)) + { + Directory.CreateDirectory(profilePath); + } + + if (!Directory.Exists(profileBackupPath)) + { + Directory.CreateDirectory(profileBackupPath); + } + + var di = new DirectoryInfo(profileBackupPath); + + if (currentProfile.brokenSaveData && File.Exists(saveBackupPath)) + { + currentProfile.gameSave = LoadAndCopyBackupSave(_gameSaveFilename, saveBackupPath, savePath); + } + + if (currentProfile.brokenMultSaveData && File.Exists(multSaveBackupPath)) + { + currentProfile.multiplayerGameSave = LoadAndCopyBackupSave(_gameSaveMultFilename, multSaveBackupPath, multSavePath); + } + + if (currentProfile.brokenSettingsData && File.Exists(settingsBackupPath)) + { + currentProfile.settingsSave = LoadAndCopyBackupSave(_gameSettingsFilename, settingsBackupPath, settingsPath); + } + + if (currentProfile.brokenGfxSettingsData && File.Exists(graphicsBackupPath)) + { + currentProfile.graphicsSettings = LoadAndCopyBackupSave(_gfxSettingsFilename, graphicsBackupPath, graphicsPath); + } + + if (currentProfile.brokenRebindingData && File.Exists(inputsBackupPath)) + { + TryLoadInputBindingsSave(ref stream, di, out var inputJSON); + if (inputJSON != "") + { + currentProfile.inputJSON = inputJSON; + File.Copy(inputsBackupPath, inputsPath, overwrite: true); + } + else + { + Debug.LogError("Could not load backup input bindings save."); + } + + stream?.Close(); + stream = null; + } + + OnBackupDataRestored?.Invoke(); + + T LoadAndCopyBackupSave(string fileName, string backupPath, string fullPath) where T : class + { + TryLoadSaveData(ref stream, fileName, di, out var saveData); + if (saveData != null) + { + File.Copy(backupPath, fullPath, overwrite: true); + } + else + { + Debug.LogError("Could not load backup " + typeof(T).Name + " save."); + } + + stream?.Close(); + stream = null; + return saveData; + } + } + catch (Exception ex) + { + stream?.Close(); + Debug.LogError("Exception during backup restore: " + ex.Message); + MarkBusyWithFileOps(isBusy: false); + } + + MarkBusyWithFileOps(isBusy: false); + } + + private bool TrySaveProfile(QSBProfileData profileData, GameSave gameSave, SettingsSave settingsSave, GraphicSettings graphicsSettings, string inputJson) + { + MarkBusyWithFileOps(isBusy: true); + var profilePath = _profilesPath + "/" + profileData.profileName; + var profileManifestPath = _profilesPath + "/" + profileData.profileName + ".owprofile"; + var saveDataPath = profilePath + "/" + _gameSaveFilename; + var multSaveDataPath = profilePath + "/" + _gameSaveMultFilename; + var settingsPath = profilePath + "/" + _gameSettingsFilename; + var graphicsPath = profilePath + "/" + _gfxSettingsFilename; + var inputsPath = profilePath + "/" + _inputActionsSettingsFilename; + + var tempProfilePath = _profileTempPath + "/GameData"; + var tempProfileManifestPath = _profileTempPath + "/CurrentProfile.owprofile"; + var tempSaveDataPath = tempProfilePath + "/" + _gameSaveFilename; + var tempMultSaveDataPath = tempProfilePath + "/" + _gameSaveMultFilename; + var tempSettingsPath = tempProfilePath + "/" + _gameSettingsFilename; + var tempGraphicsPath = tempProfilePath + "/" + _gfxSettingsFilename; + var tempInputsPath = tempProfilePath + "/" + _inputActionsSettingsFilename; + + var backupProfilePath = _profileBackupPath + "/" + profileData.profileName; + var backupSaveDataPath = backupProfilePath + "/" + _gameSaveFilename; + var backupMultSaveDataPath = backupProfilePath + "/" + _gameSaveMultFilename; + var backupSettingsPath = backupProfilePath + "/" + _gameSettingsFilename; + var backupGraphicsPath = backupProfilePath + "/" + _gfxSettingsFilename; + var backupInputsPath = backupProfilePath + "/" + _inputActionsSettingsFilename; + + Stream stream = null; + try + { + // Create folders if they don't exist + + if (!Directory.Exists(_profilesPath)) + { + Directory.CreateDirectory(_profilesPath); + } + + if (!Directory.Exists(_profileTempPath)) + { + Directory.CreateDirectory(_profileTempPath); + } + + if (!Directory.Exists(_profileBackupPath)) + { + Directory.CreateDirectory(_profileBackupPath); + } + + if (!Directory.Exists(profilePath)) + { + Directory.CreateDirectory(profilePath); + } + + if (!Directory.Exists(tempProfilePath)) + { + Directory.CreateDirectory(tempProfilePath); + } + + if (!Directory.Exists(backupProfilePath)) + { + Directory.CreateDirectory(backupProfilePath); + } + + // create temp files + + SaveData(tempProfileManifestPath, profileData); + if (gameSave != null) + { + if (QSBCore.IsInMultiplayer) + { + profileData.multiplayerGameSave = SaveData(tempMultSaveDataPath, gameSave); + } + else + { + profileData.gameSave = SaveData(tempSaveDataPath, gameSave); + } + } + + if (settingsSave != null) + { + profileData.settingsSave = SaveData(tempSettingsPath, settingsSave); + } + + if (graphicsSettings != null) + { + profileData.graphicsSettings = SaveData(tempGraphicsPath, graphicsSettings); + } + + if (inputJson != null) + { + File.WriteAllText(tempInputsPath, inputJson); + profileData.inputJSON = inputJson; + } + + // create backups of old files + + if (File.Exists(saveDataPath)) + { + File.Copy(saveDataPath, backupSaveDataPath, overwrite: true); + } + + if (File.Exists(multSaveDataPath)) + { + File.Copy(multSaveDataPath, backupMultSaveDataPath, overwrite: true); + } + + if (File.Exists(settingsPath)) + { + File.Copy(settingsPath, backupSettingsPath, overwrite: true); + } + + if (File.Exists(graphicsPath)) + { + File.Copy(graphicsPath, backupGraphicsPath, overwrite: true); + } + + if (File.Exists(inputsPath)) + { + File.Copy(inputsPath, backupInputsPath, overwrite: true); + } + + // delete old files and move temp files + + File.Delete(profileManifestPath); + File.Move(tempProfileManifestPath, profileManifestPath); + + if (gameSave != null) + { + if (QSBCore.IsInMultiplayer) + { + File.Delete(multSaveDataPath); + File.Move(tempMultSaveDataPath, multSaveDataPath); + } + else + { + File.Delete(saveDataPath); + File.Move(tempSaveDataPath, saveDataPath); + } + } + + if (settingsSave != null) + { + File.Delete(settingsPath); + File.Move(tempSettingsPath, settingsPath); + } + + if (graphicsSettings != null) + { + File.Delete(graphicsPath); + File.Move(tempGraphicsPath, graphicsPath); + } + + if (inputJson != null) + { + File.Delete(inputsPath); + File.Move(tempInputsPath, inputsPath); + } + + OnProfileDataSaved?.Invoke(true); + } + catch (Exception ex) + { + if (stream != null) + { + stream.Close(); + } + + OnProfileDataSaved?.Invoke(false); + + Debug.LogError("[" + ex.Message + "] Error saving file for " + profileData.profileName); + MarkBusyWithFileOps(isBusy: false); + return false; + } + + MarkBusyWithFileOps(isBusy: false); + return true; + + T SaveData(string filePath, T data) + { + stream = File.Open(filePath, FileMode.Create); + using (JsonWriter jsonWriter = new JsonTextWriter(new StreamWriter(stream))) + { + _jsonSerializer.Serialize(jsonWriter, data); + } + + stream = null; + return data; + } + } + + public bool IsValidCharacterForProfileName(char inputChar) + { + if (char.IsWhiteSpace(inputChar)) + { + return false; + } + + var invalidFileNameChars = Path.GetInvalidFileNameChars(); + for (var i = 0; i < invalidFileNameChars.Length; i++) + { + if (invalidFileNameChars[i] == inputChar) + { + return false; + } + } + + return inputChar != '.'; + } + + public bool ValidateProfileName(string profileName) + { + var result = true; + if (profileName == "") + { + result = false; + } + else if (profileName.Length > 16) + { + result = false; + } + else if (profiles.Count > 0) + { + for (var i = 0; i < profiles.Count; i++) + { + if (profiles[i].profileName == profileName) + { + result = false; + } + } + } + + return result; + } + + public bool TryCreateProfile(string profileName) + { + var savedProfile = ValidateProfileName(profileName); + if (savedProfile) + { + var noProfilesExist = profiles.Count == 0; + var profileData = new QSBProfileData + { + profileName = profileName, + lastModifiedTime = DateTime.UtcNow + }; + var gameSave = new GameSave(); + var multGameSave = new GameSave(); + var settingsSave = new SettingsSave(); + var graphicSettings = currentProfileGraphicsSettings; + if (graphicSettings == null) + { + graphicSettings = new GraphicSettings(init: true); + } + + var text = ((InputManager)OWInput.SharedInputManager).commandManager.DefaultInputActions.ToJson(); + profiles.Add(profileData); + profileData.gameSave = gameSave; + profileData.multiplayerGameSave = multGameSave; + profileData.settingsSave = settingsSave; + profileData.graphicsSettings = graphicSettings; + profileData.inputJSON = text; + savedProfile = TrySaveProfile(profileData, gameSave, settingsSave, graphicSettings, text); + if (savedProfile) + { + if (currentProfile != null && currentProfile.profileName != string.Empty) + { + OnProfileSignOutComplete?.Invoke(); + } + + currentProfile = profileData; + if (noProfilesExist) + { + OnProfileSignInComplete?.Invoke(ProfileManagerSignInResult.COMPLETE); + OnProfileReadDone?.Invoke(); + } + else + { + OnProfileSignInComplete?.Invoke(ProfileManagerSignInResult.COMPLETE); + OnProfileReadDone?.Invoke(); + OnUpdatePlayerProfiles?.Invoke(); + } + } + else + { + DeleteProfile(profileName); + } + } + + return savedProfile; + } + + public bool SwitchProfile(string profileName) + { + LoadSaveFilesFromProfiles(); + var flag = false; + for (var i = 0; i < profiles.Count; i++) + { + if (profileName == profiles[i].profileName) + { + if (currentProfile != null && currentProfile.profileName != string.Empty && OnProfileSignOutComplete != null) + { + OnProfileSignOutComplete(); + } + + currentProfile = profiles[i]; + flag = true; + break; + } + } + + if (flag) + { + currentProfile.lastModifiedTime = DateTime.UtcNow; + TrySaveProfile(currentProfile, null, null, null, null); + OnProfileSignInComplete?.Invoke(ProfileManagerSignInResult.COMPLETE); + + if (CurrentProfileHasBrokenData() && OnBrokenDataExists != null) + { + OnBrokenDataExists(); + return false; + } + + OnProfileReadDone?.Invoke(); + } + + return true; + } + + public void DeleteProfile(string profileName) + { + Debug.Log("DeleteProfile"); + var flag = false; + var profileData = new QSBProfileData + { + profileName = string.Empty + }; + for (var i = 0; i < profiles.Count; i++) + { + if (profileName == profiles[i].profileName) + { + profileData = profiles[i]; + flag = true; + break; + } + } + + if (!flag) + { + return; + } + + MarkBusyWithFileOps(isBusy: true); + var profileManifestPath = _profilesPath + "/" + profileData.profileName + ".owprofile"; + var profilePath = _profilesPath + "/" + profileData.profileName; + var gameSavePath = profilePath + "/" + _gameSaveFilename; + var multGameSavePath = profilePath + "/" + _gameSaveMultFilename; + var settingsPath = profilePath + "/" + _gameSettingsFilename; + var graphicsPath = profilePath + "/" + _gfxSettingsFilename; + var oldInputsPath = profilePath + "/" + _legacyInputBindingSettingsFilename; + var inputsPath = profilePath + "/" + _inputActionsSettingsFilename; + + var backupProfilePath = _profileBackupPath + "/" + profileData.profileName; + var backupGameSave = backupProfilePath + "/" + _gameSaveFilename; + var backupMultGameSave = backupProfilePath + "/" + _gameSaveMultFilename; + var backupSettingsPath = backupProfilePath + "/" + _gameSettingsFilename; + var backupGraphicsPath = backupProfilePath + "/" + _gfxSettingsFilename; + var backupOldInputsPath = backupProfilePath + "/" + _legacyInputBindingSettingsFilename; + var backupInputsPath = backupProfilePath + "/" + _inputActionsSettingsFilename; + Stream stream = null; + try + { + if (File.Exists(profileManifestPath)) + { + File.Delete(profileManifestPath); + Debug.Log("Delete " + profileManifestPath); + } + + if (File.Exists(gameSavePath)) + { + File.Delete(gameSavePath); + Debug.Log("Delete " + gameSavePath); + } + + if (File.Exists(multGameSavePath)) + { + File.Delete(multGameSavePath); + Debug.Log("Delete " + multGameSavePath); + } + + if (File.Exists(settingsPath)) + { + File.Delete(settingsPath); + Debug.Log("Delete " + settingsPath); + } + + if (File.Exists(graphicsPath)) + { + File.Delete(graphicsPath); + Debug.Log("Delete " + graphicsPath); + } + + if (File.Exists(oldInputsPath)) + { + File.Delete(oldInputsPath); + Debug.Log("Delete " + oldInputsPath); + } + + if (File.Exists(inputsPath)) + { + File.Delete(inputsPath); + Debug.Log("Delete " + inputsPath); + } + + if (File.Exists(backupGameSave)) + { + File.Delete(backupGameSave); + Debug.Log("Delete " + backupGameSave); + } + + if (File.Exists(backupMultGameSave)) + { + File.Delete(backupMultGameSave); + Debug.Log("Delete " + backupMultGameSave); + } + + if (File.Exists(backupSettingsPath)) + { + File.Delete(backupSettingsPath); + Debug.Log("Delete " + backupSettingsPath); + } + + if (File.Exists(backupGraphicsPath)) + { + File.Delete(backupGraphicsPath); + Debug.Log("Delete " + backupGraphicsPath); + } + + if (File.Exists(backupOldInputsPath)) + { + File.Delete(backupOldInputsPath); + Debug.Log("Delete " + backupOldInputsPath); + } + + if (File.Exists(backupInputsPath)) + { + File.Delete(backupInputsPath); + Debug.Log("Delete " + backupInputsPath); + } + + profiles.Remove(profileData); + var files = Directory.GetFiles(profilePath); + var directories = Directory.GetDirectories(profilePath); + if (files.Length == 0 && directories.Length == 0) + { + Directory.Delete(profilePath); + } + else + { + Debug.LogWarning(" Directory not empty. Cannot delete. "); + } + + if (Directory.Exists(backupProfilePath)) + { + files = Directory.GetFiles(backupProfilePath); + directories = Directory.GetDirectories(backupProfilePath); + if (files.Length == 0 && directories.Length == 0) + { + Directory.Delete(backupProfilePath); + } + else + { + Debug.LogWarning("Backup Directory not empty. Cannot delete."); + } + } + + OnUpdatePlayerProfiles?.Invoke(); + } + catch (Exception ex) + { + stream?.Close(); + Debug.LogError("[" + ex.Message + "] Failed to delete all profile data"); + MarkBusyWithFileOps(isBusy: false); + } + + MarkBusyWithFileOps(isBusy: false); + } + + [Serializable] + public class QSBProfileData + { + public string profileName; + public DateTime lastModifiedTime; + public bool brokenSaveData; + public bool brokenMultSaveData; + public bool brokenSettingsData; + public bool brokenGfxSettingsData; + public bool brokenRebindingData; + private GameSave _gameSave; + private GameSave _multiplayerGameSave; + private SettingsSave _settingsSave; + private GraphicSettings _graphicsSettings; + private string _inputJSON; + + [JsonIgnore] + public GameSave gameSave + { + get => _gameSave; + set => _gameSave = value; + } + + + [JsonIgnore] + public GameSave multiplayerGameSave + { + get => _multiplayerGameSave; + set => _multiplayerGameSave = value; + } + + [JsonIgnore] + public SettingsSave settingsSave + { + get => _settingsSave; + set => _settingsSave = value; + } + + [JsonIgnore] + public GraphicSettings graphicsSettings + { + get => _graphicsSettings; + set => _graphicsSettings = value; + } + + [JsonIgnore] + public string inputJSON + { + get => _inputJSON; + set => _inputJSON = value; + } + + [OnDeserializing] + private void SetDefaultValuesOnDeserializing(StreamingContext context) + { + brokenSaveData = false; + brokenMultSaveData = false; + brokenSettingsData = false; + brokenGfxSettingsData = false; + brokenRebindingData = false; + } + + [OnDeserialized] + private void SetDefaultValuesOnDeserialized(StreamingContext context) + { + brokenSaveData = false; + brokenMultSaveData = false; + brokenSettingsData = false; + brokenGfxSettingsData = false; + brokenRebindingData = false; + } + } +} diff --git a/QSB/SectorSync/WorldObjects/QSBSector.cs b/QSB/SectorSync/WorldObjects/QSBSector.cs index 28d91286..6dcce996 100644 --- a/QSB/SectorSync/WorldObjects/QSBSector.cs +++ b/QSB/SectorSync/WorldObjects/QSBSector.cs @@ -23,8 +23,6 @@ public class QSBSector : WorldObject } } - public override void SendInitialState(uint to) { } - private static EyeShuttleController _cachedShuttleController; public bool ShouldSyncTo(DynamicOccupant occupantType) diff --git a/QSB/ShipSync/Messages/FlyShipMessage.cs b/QSB/ShipSync/Messages/FlyShipMessage.cs index 60e8baa6..144efd70 100644 --- a/QSB/ShipSync/Messages/FlyShipMessage.cs +++ b/QSB/ShipSync/Messages/FlyShipMessage.cs @@ -8,6 +8,9 @@ using UnityEngine; namespace QSB.ShipSync.Messages; +/// +/// TODO: initial state for the current flyer +/// internal class FlyShipMessage : QSBMessage { static FlyShipMessage() diff --git a/QSB/ShipSync/ShipCustomAttach.cs b/QSB/ShipSync/ShipCustomAttach.cs index 6ce9adc2..e8106d09 100644 --- a/QSB/ShipSync/ShipCustomAttach.cs +++ b/QSB/ShipSync/ShipCustomAttach.cs @@ -1,12 +1,22 @@ -using UnityEngine; +using QSB.Localization; +using UnityEngine; namespace QSB.ShipSync; public class ShipCustomAttach : MonoBehaviour { - private static readonly ScreenPrompt _attachPrompt = new(InputLibrary.interactSecondary, InputLibrary.interact, - "Attach to ship" + " ", ScreenPrompt.MultiCommandType.HOLD_ONE_AND_PRESS_2ND); - private static readonly ScreenPrompt _detachPrompt = new(InputLibrary.cancel, "Detach from ship" + " "); + private readonly ScreenPrompt _attachPrompt = new( + InputLibrary.interactSecondary, + InputLibrary.interact, + QSBLocalization.Current.AttachToShip + " ", + ScreenPrompt.MultiCommandType.HOLD_ONE_AND_PRESS_2ND + ); + + private readonly ScreenPrompt _detachPrompt = new( + InputLibrary.cancel, + QSBLocalization.Current.DetachFromShip + " " + ); + private PlayerAttachPoint _playerAttachPoint; private void Awake() diff --git a/QSB/ShipSync/ShipManager.cs b/QSB/ShipSync/ShipManager.cs index aa88ca0f..c85a2751 100644 --- a/QSB/ShipSync/ShipManager.cs +++ b/QSB/ShipSync/ShipManager.cs @@ -42,15 +42,28 @@ internal class ShipManager : WorldObjectManager _currentFlyer = value; } } + public bool IsShipWrecked => _shipDestroyed || ShipCockpitUI._shipDamageCtrlr.IsDestroyed(); private readonly List _playersInShip = new(); private uint _currentFlyer = uint.MaxValue; + private bool _shipDestroyed; public void Start() { Instance = this; QSBPlayerManager.OnRemovePlayer += OnRemovePlayer; + GlobalMessenger.AddListener("ShipDestroyed", OnShipDestroyed); + } + + public void OnDestroy() + { + GlobalMessenger.RemoveListener("ShipDestroyed", OnShipDestroyed); + } + + private void OnShipDestroyed() + { + _shipDestroyed = true; } private void OnRemovePlayer(PlayerInfo player) @@ -63,6 +76,8 @@ internal class ShipManager : WorldObjectManager public override async UniTask BuildWorldObjects(OWScene scene, CancellationToken ct) { + _shipDestroyed = false; + var shipBody = Locator.GetShipBody(); if (shipBody == null) { @@ -89,17 +104,6 @@ internal class ShipManager : WorldObjectManager if (QSBCore.IsHost) { - if (ShipTransformSync.LocalInstance != null) - { - if (ShipTransformSync.LocalInstance.gameObject == null) - { - DebugLog.ToConsole($"Warning - ShipTransformSync's LocalInstance is not null, but it's gameobject is null!", MessageType.Warning); - return; - } - - NetworkServer.Destroy(ShipTransformSync.LocalInstance.gameObject); - } - if (QSBPlayerManager.LocalPlayer.TransformSync == null) { DebugLog.ToConsole($"Error - Tried to spawn ship, but LocalPlayer's TransformSync is null!", MessageType.Error); @@ -115,7 +119,7 @@ internal class ShipManager : WorldObjectManager _shipCustomAttach.transform.SetParent(shipBody.transform, false); _shipCustomAttach.AddComponent(); - QSBWorldSync.Init(new List() + QSBWorldSync.Init(new[] { CockpitController._headlight, CockpitController._landingLight, @@ -134,7 +138,24 @@ internal class ShipManager : WorldObjectManager QSBWorldSync.Init(); } - public override void UnbuildWorldObjects() => Destroy(_shipCustomAttach); + public override void UnbuildWorldObjects() + { + if (QSBCore.IsHost) + { + if (ShipTransformSync.LocalInstance != null) + { + if (ShipTransformSync.LocalInstance.gameObject == null) + { + DebugLog.ToConsole($"Warning - ShipTransformSync's LocalInstance is not null, but it's gameobject is null!", MessageType.Warning); + return; + } + + NetworkServer.Destroy(ShipTransformSync.LocalInstance.gameObject); + } + } + + Destroy(_shipCustomAttach); + } public void AddPlayerToShip(PlayerInfo player) { @@ -218,4 +239,4 @@ internal class ShipManager : WorldObjectManager CockpitController._landingCam.enabled = false; CockpitController._shipAudioController.PlayLandingCamOff(); } -} \ No newline at end of file +} diff --git a/QSB/ShipSync/TransformSync/ShipTransformSync.cs b/QSB/ShipSync/TransformSync/ShipTransformSync.cs index 8abe29dd..29d6597b 100644 --- a/QSB/ShipSync/TransformSync/ShipTransformSync.cs +++ b/QSB/ShipSync/TransformSync/ShipTransformSync.cs @@ -57,13 +57,20 @@ public class ShipTransformSync : SectoredRigidbodySync } var targetPos = ReferenceTransform.FromRelPos(transform.position); + var targetRot = ReferenceTransform.FromRelRot(transform.rotation); - if (Time.unscaledTime >= _lastSetPositionTime + ForcePositionAfterTime) + if (PlayerState.IsInsideShip()) { - _lastSetPositionTime = Time.unscaledTime; - - var targetRot = ReferenceTransform.FromRelRot(transform.rotation); + if (Time.unscaledTime >= _lastSetPositionTime + ForcePositionAfterTime) + { + _lastSetPositionTime = Time.unscaledTime; + AttachedRigidbody.SetPosition(targetPos); + AttachedRigidbody.SetRotation(targetRot); + } + } + else + { AttachedRigidbody.SetPosition(targetPos); AttachedRigidbody.SetRotation(targetRot); } diff --git a/QSB/ShipSync/WorldObjects/QSBShipDetachableLeg.cs b/QSB/ShipSync/WorldObjects/QSBShipDetachableLeg.cs index cd49af02..e75b0c01 100644 --- a/QSB/ShipSync/WorldObjects/QSBShipDetachableLeg.cs +++ b/QSB/ShipSync/WorldObjects/QSBShipDetachableLeg.cs @@ -16,6 +16,6 @@ internal class QSBShipDetachableLeg : LinkedWorldObject { if (AttachedObject._damaged) { - this.SendMessage(new HullDamagedMessage()); + this.SendMessage(new HullDamagedMessage { To = to }); } else { - this.SendMessage(new HullRepairedMessage()); + this.SendMessage(new HullRepairedMessage { To = to }); } - this.SendMessage(new HullChangeIntegrityMessage(AttachedObject._integrity)); + this.SendMessage(new HullChangeIntegrityMessage(AttachedObject._integrity) { To = to }); } public void SetDamaged() @@ -58,4 +58,4 @@ internal class QSBShipHull : WorldObject var damageEffect = AttachedObject._damageEffect; damageEffect.SetEffectBlend(1f - newIntegrity); } -} \ No newline at end of file +} diff --git a/QSB/Syncs/Occasional/OccasionalTransformSync.cs b/QSB/Syncs/Occasional/OccasionalTransformSync.cs index 9d265983..c4ba37f9 100644 --- a/QSB/Syncs/Occasional/OccasionalTransformSync.cs +++ b/QSB/Syncs/Occasional/OccasionalTransformSync.cs @@ -105,8 +105,8 @@ public class OccasionalTransformSync : UnsectoredRigidbodySync _toMove.Add(new MoveData { Child = child, - RelPos = AttachedRigidbody.transform.ToRelPos(pos), - RelRot = AttachedRigidbody.transform.ToRelRot(child.GetRotation()), + RelPos = AttachedTransform.ToRelPos(pos), + RelRot = AttachedTransform.ToRelRot(child.GetRotation()), RelVel = AttachedRigidbody.ToRelVel(child.GetVelocity(), pos), RelAngVel = AttachedRigidbody.ToRelAngVel(child.GetAngularVelocity()) }); @@ -116,9 +116,9 @@ public class OccasionalTransformSync : UnsectoredRigidbodySync { foreach (var data in _toMove) { - var pos = AttachedRigidbody.transform.FromRelPos(data.RelPos); + var pos = AttachedTransform.FromRelPos(data.RelPos); data.Child.SetPosition(pos); - data.Child.SetRotation(AttachedRigidbody.transform.FromRelRot(data.RelRot)); + data.Child.SetRotation(AttachedTransform.FromRelRot(data.RelRot)); data.Child.SetVelocity(AttachedRigidbody.FromRelVel(data.RelVel, pos)); data.Child.SetAngularVelocity(AttachedRigidbody.FromRelAngVel(data.RelAngVel)); } diff --git a/QSB/TimeSync/Messages/ServerTimeMessage.cs b/QSB/TimeSync/Messages/ServerTimeMessage.cs index 3be1812c..043074a6 100644 --- a/QSB/TimeSync/Messages/ServerTimeMessage.cs +++ b/QSB/TimeSync/Messages/ServerTimeMessage.cs @@ -7,11 +7,13 @@ public class ServerTimeMessage : QSBMessage { private float ServerTime; private int LoopCount; + private float SecondsRemaining; - public ServerTimeMessage(float time, int count) + public ServerTimeMessage(float time, int count, float secondsRemaining) { ServerTime = time; LoopCount = count; + SecondsRemaining = secondsRemaining; } public override void Serialize(NetworkWriter writer) @@ -19,6 +21,7 @@ public class ServerTimeMessage : QSBMessage base.Serialize(writer); writer.Write(ServerTime); writer.Write(LoopCount); + writer.Write(SecondsRemaining); } public override void Deserialize(NetworkReader reader) @@ -26,8 +29,9 @@ public class ServerTimeMessage : QSBMessage base.Deserialize(reader); ServerTime = reader.Read(); LoopCount = reader.Read(); + SecondsRemaining = reader.Read(); } public override void OnReceiveRemote() - => WakeUpSync.LocalInstance.OnClientReceiveMessage(ServerTime, LoopCount); -} \ No newline at end of file + => WakeUpSync.LocalInstance.OnClientReceiveMessage(ServerTime, LoopCount, SecondsRemaining); +} diff --git a/QSB/TimeSync/Messages/SetSecondsRemainingMessage.cs b/QSB/TimeSync/Messages/SetSecondsRemainingMessage.cs new file mode 100644 index 00000000..c8fbe274 --- /dev/null +++ b/QSB/TimeSync/Messages/SetSecondsRemainingMessage.cs @@ -0,0 +1,13 @@ +using QSB.Messaging; +using QSB.Patches; + +namespace QSB.TimeSync.Messages; + +/// +/// sent from non-host to host +/// +public class SetSecondsRemainingMessage : QSBMessage +{ + public SetSecondsRemainingMessage(float secondsRemaining) : base(secondsRemaining) => To = 0; + public override void OnReceiveRemote() => QSBPatch.RemoteCall(() => TimeLoop.SetSecondsRemaining(Data)); +} diff --git a/QSB/TimeSync/Patches/TimePatches.cs b/QSB/TimeSync/Patches/TimePatches.cs index 2763ac99..2c21229a 100644 --- a/QSB/TimeSync/Patches/TimePatches.cs +++ b/QSB/TimeSync/Patches/TimePatches.cs @@ -1,6 +1,8 @@ using HarmonyLib; using QSB.Inputs; +using QSB.Messaging; using QSB.Patches; +using QSB.TimeSync.Messages; using QSB.Utility; namespace QSB.TimeSync.Patches; @@ -10,13 +12,13 @@ internal class TimePatches : QSBPatch { public override QSBPatchTypes Type => QSBPatchTypes.OnClientConnect; + /// + /// prevents wakeup prompt since we automatically wake you up. + /// (doesn't happen for host because we don't patch until TimeLoop._initialized i.e. after Start) + /// [HarmonyPrefix] [HarmonyPatch(typeof(PlayerCameraEffectController), nameof(PlayerCameraEffectController.OnStartOfTimeLoop))] - public static bool PlayerCameraEffectController_OnStartOfTimeLoop() - { - DebugLog.DebugWrite($"OnStartOfTimeLoop"); - return false; - } + public static bool PlayerCameraEffectController_OnStartOfTimeLoop() => false; [HarmonyPostfix] [HarmonyPatch(typeof(PlayerCameraEffectController), nameof(PlayerCameraEffectController.WakeUp))] @@ -27,7 +29,6 @@ internal class TimePatches : QSBPatch Delay.RunWhen(() => !__instance._isOpeningEyes, () => QSBInputManager.Instance.SetInputsEnabled(true)); } - [HarmonyPrefix] [HarmonyPatch(typeof(OWTime), nameof(OWTime.Pause))] public static bool StopPausing(OWTime.PauseType pauseType) @@ -40,4 +41,20 @@ internal class TimePatches : QSBPatch [HarmonyPatch(typeof(SubmitActionSkipToNextLoop), nameof(SubmitActionSkipToNextLoop.AdvanceToNewTimeLoop))] public static bool StopMeditation() => false; -} \ No newline at end of file +} + +internal class ClientTimePatches : QSBPatch +{ + public override QSBPatchTypes Type => QSBPatchTypes.OnNonServerClientConnect; + + [HarmonyPrefix] + [HarmonyPatch(typeof(TimeLoop), nameof(TimeLoop.SetSecondsRemaining))] + private static void SetSecondsRemaining(float secondsRemaining) + { + if (Remote) + { + return; + } + new SetSecondsRemainingMessage(secondsRemaining).Send(); + } +} diff --git a/QSB/TimeSync/WakeUpSync.cs b/QSB/TimeSync/WakeUpSync.cs index 29abf702..9b8ef589 100644 --- a/QSB/TimeSync/WakeUpSync.cs +++ b/QSB/TimeSync/WakeUpSync.cs @@ -5,6 +5,7 @@ using QSB.ClientServerStateSync.Messages; using QSB.DeathSync; using QSB.Inputs; using QSB.Messaging; +using QSB.Patches; using QSB.Player; using QSB.Player.Messages; using QSB.TimeSync.Messages; @@ -129,6 +130,7 @@ public class WakeUpSync : NetworkBehaviour } else { + // dont bother sleeping, just wake up if (!_hasWokenUp) { Delay.RunWhen(() => QSBWorldSync.AllObjectsReady, WakeUp); @@ -138,12 +140,13 @@ public class WakeUpSync : NetworkBehaviour } private void SendServerTime() - => new ServerTimeMessage(_serverTime, PlayerData.LoadLoopCount()).Send(); + => new ServerTimeMessage(_serverTime, PlayerData.LoadLoopCount(), TimeLoop.GetSecondsRemaining()).Send(); - public void OnClientReceiveMessage(float time, int count) + public void OnClientReceiveMessage(float time, int count, float secondsRemaining) { _serverTime = time; _serverLoopCount = count; + QSBPatch.RemoteCall(() => TimeLoop.SetSecondsRemaining(secondsRemaining)); } private void WakeUpOrSleep() @@ -162,7 +165,7 @@ public class WakeUpSync : NetworkBehaviour var myTime = Time.timeSinceLevelLoad; var diff = myTime - _serverTime; - if (ServerStateManager.Instance.GetServerState() is not ServerState.InSolarSystem and not ServerState.InEye) + if (ServerStateManager.Instance.GetServerState() is not (ServerState.InSolarSystem or ServerState.InEye)) { return; } @@ -170,13 +173,19 @@ public class WakeUpSync : NetworkBehaviour if (diff > PauseOrFastForwardThreshold) { StartPausing(PauseReason.TooFarAhead); - return; } - - if (diff < -PauseOrFastForwardThreshold) + else if (diff < -PauseOrFastForwardThreshold) { StartFastForwarding(FastForwardReason.TooFarBehind); } + else + { + // should only happen from Init so we gotta wait + if (!_hasWokenUp) + { + Delay.RunWhen(() => QSBWorldSync.AllObjectsReady, WakeUp); + } + } } private void StartFastForwarding(FastForwardReason reason) @@ -294,7 +303,8 @@ public class WakeUpSync : NetworkBehaviour if (CurrentState == State.Pausing && (PauseReason)CurrentReason == PauseReason.WaitingForAllPlayersToBeReady) { - if (clientState == ClientState.AliveInSolarSystem && serverState == ServerState.InSolarSystem) + if ((clientState == ClientState.AliveInSolarSystem && serverState == ServerState.InSolarSystem) || + (clientState == ClientState.AliveInEye && serverState == ServerState.InEye)) { ResetTimeScale(); } @@ -390,7 +400,8 @@ public class WakeUpSync : NetworkBehaviour if (CurrentState == State.Pausing && (PauseReason)CurrentReason == PauseReason.WaitingForAllPlayersToBeReady) { - if (clientState == ClientState.AliveInSolarSystem && serverState == ServerState.InSolarSystem) + if ((clientState == ClientState.AliveInSolarSystem && serverState == ServerState.InSolarSystem) || + (clientState == ClientState.AliveInEye && serverState == ServerState.InEye)) { ResetTimeScale(); } @@ -422,7 +433,7 @@ public class WakeUpSync : NetworkBehaviour { var diff = GetTimeDifference(); - if (diff is > PauseOrFastForwardThreshold or < (-PauseOrFastForwardThreshold)) + if (diff is > PauseOrFastForwardThreshold or < -PauseOrFastForwardThreshold) { WakeUpOrSleep(); return; diff --git a/QSB/Translations/en.json b/QSB/Translations/en.json index 68314f46..e0605cfc 100644 --- a/QSB/Translations/en.json +++ b/QSB/Translations/en.json @@ -4,13 +4,16 @@ "MainMenuConnect": "CONNECT TO MULTIPLAYER", "PauseMenuDisconnect": "DISCONNECT", "PauseMenuStopHosting": "STOP HOSTING", - "PublicIPAddress": "Public IP Address\n\n(YOUR SAVE DATA WILL BE OVERWRITTEN)", - "ProductUserID": "Product User ID\n\n(YOUR SAVE DATA WILL BE OVERWRITTEN)", + "PublicIPAddress": "Public IP Address\n\n(YOUR MULTIPLAYER SAVE DATA WILL BE OVERWRITTEN)", + "ProductUserID": "Product User ID\n\n(YOUR MULTIPLAYER SAVE DATA WILL BE OVERWRITTEN)", "Connect": "CONNECT", "Cancel": "CANCEL", - "HostExistingOrNew": "Do you want to host an existing expedition, or host a new expedition?", + "HostExistingOrNewOrCopy": "Do you want to host an existing multiplayer expedition, host a new expedition, or copy the existing singleplayer expedition to multiplayer?", + "HostNewOrCopy": "Do you want to host a new expedition, or copy the existing singleplayer expedition to multiplayer?", + "HostExistingOrNew": "Do you want to host an existing multiplayer expedition, or host a new expedition?", "ExistingSave": "EXISTING SAVE", "NewSave": "NEW SAVE", + "CopySave": "COPY SAVE", "DisconnectAreYouSure": "Are you sure you want to disconnect?\nThis will send you back to the main menu.", "Yes": "YES", "No": "NO", @@ -36,6 +39,11 @@ "TimeSyncWaitForAllToReady": "Waiting for start of loop...", "TimeSyncWaitForAllToDie": "Waiting for end of loop...", "GalaxyMapEveryoneNotPresent": "It's not yet time. Everyone should be here to witness this.", + "YouAreDead": "You are dead.", + "WaitingForRespawn": "Waiting for someone to respawn you...", + "WaitingForAllToDie": "Waiting for {0} player(s) to die...", + "AttachToShip": "Attach to ship", + "DetachFromShip": "Detach from ship", "DeathMessages": { "Default": [ "{0} died", diff --git a/QSB/Translations/fr.json b/QSB/Translations/fr.json index 68ff74e6..f1fda671 100644 --- a/QSB/Translations/fr.json +++ b/QSB/Translations/fr.json @@ -4,13 +4,16 @@ "MainMenuConnect": "SE CONNECTER AU MULTIJOUEUR", "PauseMenuDisconnect": "DÉCONNECTER", "PauseMenuStopHosting": "ARRÊTEZ L'HÉBERGEMENT", - "PublicIPAddress": "Adresse IP publique\n\n(CELA EFFACERA VOTRE PROGRESSION)", - "ProductUserID": "ID utilisateur\n\n(CELA EFFACERA VOTRE PROGRESSION)", + "PublicIPAddress": "Adresse IP publique\n\n(CELA EFFACERA VOTRE PROGRESSION MULTIJOUEUR)", + "ProductUserID": "ID utilisateur\n\n(CELA EFFACERA VOTRE PROGRESSION MULTIJOUEUR)", "Connect": "SE CONNECTER", "Cancel": "ANNULER", - "HostExistingOrNew": "Veux-tu héberger une expédition existante, ou héberger une nouvelle expédition?", + "HostExistingOrNewOrCopy": "Veux-tu héberger une expédition multijouer existante, héberger une nouvelle expédition, ou héberger une copie de ton expédition solo existante?", + "HostNewOrCopy": "Veux-tu héberger une nouvelle expédition, ou héberger une copie de ton expédition solo existante?", + "HostExistingOrNew": "Veux-tu héberger une expédition multijouer existante, ou héberger une nouvelle expédition?", "ExistingSave": "SAUVEGARDE EXISTANTE", "NewSave": "NOUVELLE SAUVEGARDE", + "CopySave": "COPIER SAUVEGARDE", "DisconnectAreYouSure": "Veux-tu vraiment te déconnecter?\n Cela te ramènera au menu principal.", "Yes": "OUI", "No": "NON", diff --git a/QSB/Translations/pt-br.json b/QSB/Translations/pt-br.json new file mode 100644 index 00000000..7aa4aea3 --- /dev/null +++ b/QSB/Translations/pt-br.json @@ -0,0 +1,121 @@ +{ + "Language": "PORTUGUESE_BR", + "MainMenuHost": "ABRIR PARA MULTIJOGADORES", + "MainMenuConnect": "CONECTAR PARA JOGO DE MULTIJOGADORES", + "PauseMenuDisconnect": "DESCONECTAR", + "PauseMenuStopHosting": "PARAR DE HOSPEDAR", + "PublicIPAddress": "Endereço de IP Público\n\n(O DADOS DO SEU SAVE DE MULTIJOGADORES SERÃO SOBRESCRITOS)", + "ProductUserID": "ID de Produto de Usuário\n\n(O DADOS DO SEU SAVE DE MULTIJOGADORES SERÃO SOBRESCRITOS)", + "Connect": "CONECTAR", + "Cancel": "CANCELAR", + "HostExistingOrNewOrCopy": "Você quer hospedar uma expedição de multijogadores pré-existente, hospedar uma nova expedição, ou copiar a já existente expedição do seu save para multiplos jogadores?", + "HostNewOrCopy": "Você quer hospedar uma nova expedição, ou copiar a já existente expedição do seu save para multiplos jogadores?", + "HostExistingOrNew": "Você quer hospedar uma expedição de multijogadores pré-existente, ou hospedar uma nova expedição?", + "ExistingSave": "SAVE PRÉ-EXISTENTE", + "NewSave": "NOVO SAVE", + "CopySave": "COPIAR SAVE", + "DisconnectAreYouSure": "Você tem certeza que quer desconectar?\nEssa ação irá de enviar de volta ao menu principal.", + "Yes": "SIM", + "No": "NÃO", + "StopHostingAreYouSure": "Você tem certeza que quer parar de hospedar\nEssa ação irá de desconectar os jogadores conectados e eviará todos de volta ao menu principal.", + "CopyProductUserIDToClipboard": "Hospedando servidor.\nOutros jogadores poderão se conectar usando o seu ID de produto de usuário, o qual é :\n{0}\nVocê quer copia-lo para a área de transferência?", + "Connecting": "CONECTANDO...", + "OK": "OK", + "ServerRefusedConnection": "Servidor recusou a conexão.\n{0}", + "ClientDisconnectWithError": "Cliente desconectou com um erro!\n{0}", + "QSBVersionMismatch": "As versão do QSB não correspondem. (Cliente:{0}, Servidor:{1})", + "OWVersionMismatch": "As versão de Outer Wilds não correspondem. (Cliente:{0}, Servidor:{1})", + "DLCMismatch": "O estado da instalação da DLC não correspondem. (Cliente:{0}, Servidor:{1})", + "GameProgressLimit": "O jogo progrediu além do limite.", + "AddonMismatch": "Incompatibilidade de Addons. (Cliente:{0} addons, Servidor:{1} addons)", + "IncompatibleMod": "Usando um mod incompativel ou não permitido. Primeiro mod encontrado foi {0}", + "PlayerJoinedTheGame": "{0} entrou!", + "PlayerWasKicked": "{0} foi expulso.", + "KickedFromServer": "Expulso do servidor. Motivo : {0}", + "RespawnPlayer": "Renascer Jogador", + "TimeSyncTooFarBehind": "{0}\nAvançando o tempo para corresponder com o tempo do servidor...", + "TimeSyncWaitingForStartOfServer": "Esperando para o servidor começar...", + "TimeSyncTooFarAhead": "{0}\nParando para corresponder com o tempo do servidor...", + "TimeSyncWaitForAllToReady": "Esperando para o começo do loop...", + "TimeSyncWaitForAllToDie": "Esperando para o fim do loop...", + "GalaxyMapEveryoneNotPresent": "Ainda não é a hora. Todos deveriam estar aqui para presenciar isso.", + "DeathMessages": { + "Default": [ + "{0} morreu", + "{0} foi morto" + ], + "Impact": [ + "{0} esqueceu de usar os retro-foguetes", + "{0} experienciou a inércia", + "{0} bateu muito forte no chão", + "{0} virou uma panqueca", + "{0} morreu no impacto", + "{0} impactou o chão muito forte", + "{0} morreu graças a brusca desaceleração" + ], + "Asphyxiation": [ + "{0} esqueceu de respirar", + "{0} asfixiou", + "{0} moreu de asfixiação", + "{0} esqueceu de como respirar", + "{0} esqueceu de checar o seu oxigênio", + "{0} ficou sem ar", + "{0} ficou sem oxigênio", + "{0} não precisava de ar de qualquer maneira" + ], + "Energy": [ + "{0} foi cozinhado" + ], + "Supernova": [ + "{0} ficou sem tempo", + "{0} queimou", + "{0} foi cozinhado", + "{0} foi vaporizado", + "{0} perdeu a noção do tempo" + ], + "Digestion": [ + "{0} foi comido", + "{0} encontrou um peixe", + "{0} encontrou uma horrenda criatura", + "{0} se meteu com o peixe errado", + "{0} foi digerido", + "{0} morreu pela digestão" + ], + "Crushed": [ + "{0} foi esmagado", + "{0} foi amassado", + "{0} foi enterrado", + "{0} não saiu a tempo", + "{0} foi nadar na areia", + "{0} subestimou a areia", + "{0} ficou preso embaixo da areia" + ], + "Lava": [ + "{0} morreu na lava", + "{0} foi derretido", + "{0} tentou nadar na lava", + "{0} caiu na lava", + "{0} morreu pela lava", + "{0} foi nadar na lava", + "{0} acabou queimado pela lava" + ], + "BlackHole": [ + "{0} foi atrás de suas mamórias" + ], + "DreamExplosion": [ + "{0} explodiu", + "{0} foi um dos primeiros a testar", + "{0} foi frito", + "{0} morreu graças a uma explosão", + "{0} usou o artefato errado" + ], + "CrushedByElevator": [ + "{0} foi esmagado", + "{0} foi amassado", + "{0} foi esmagado por um elevador", + "{0} ficou embaixo de um elevador", + "{0} virou uma panqueca", + "{0} foi amassado por um elevador" + ] + } +} diff --git a/QSB/Translations/ru.json b/QSB/Translations/ru.json index c1c457e5..c08d646b 100644 --- a/QSB/Translations/ru.json +++ b/QSB/Translations/ru.json @@ -4,13 +4,16 @@ "MainMenuConnect": "ПОДКЛЮЧИТЬСЯ К МУЛЬТИПЛЕЕРУ", "PauseMenuDisconnect": "ОТКЛЮЧИТЬСЯ", "PauseMenuStopHosting": "ПРЕКРАТИТЬ ХОСТИНГ", - "PublicIPAddress": "Публичный IP-адрес\n\n(ВАШИ СОХРАНЁННЫЕ ДАННЫЕ БУДУТ ПЕРЕЗАПИСАНЫ)", - "ProductUserID": "ID игрока\n\n(ВАШИ СОХРАНЁННЫЕ ДАННЫЕ БУДУТ ПЕРЕЗАПИСАНЫ)", + "PublicIPAddress": "Публичный IP-адрес\n\n(ВАШИ МНОГОПОЛЬЗОВАТЕЛЬСКИЕ СОХРАНЁННЫЕ ДАННЫЕ БУДУТ ПЕРЕЗАПИСАНЫ)", + "ProductUserID": "ID игрока\n\n(ВАШИ МНОГОПОЛЬЗОВАТЕЛЬСКИЕ СОХРАНЁННЫЕ ДАННЫЕ БУДУТ ПЕРЕЗАПИСАНЫ)", "Connect": "Подключиться", "Cancel": "Отмена", + "HostExistingOrNewOrCopy": "Вы хотите продолжить существующую мультиплеерную экспедицию, начать новую мультиплеерную экспедицию, или скопировать прогресс из одиночной экспедиции?", + "HostNewOrCopy": "Вы хотите начать новую мультиплеерную экспедицию, или скопировать прогресс из одиночной экспедиции?", "HostExistingOrNew": "Вы желаете хостить сервер на существующем сохранении, или создать новое?", "ExistingSave": "СУЩЕСТВУЮЩЕЕ СОХРАНЕНИЕ", "NewSave": "НОВОЕ СОХРАНЕНИЕ", + "CopySave": "СКОПИРОВАТЬ СОХРАНЕНИЕ", "DisconnectAreYouSure": "Вы уверены, что хотите отсоедениться?\nЭто отправит вас обратно в главное меню.", "Yes": "ДА", "No": "НЕТ", diff --git a/QSB/TriggerSync/Messages/TriggerInitialStateMessage.cs b/QSB/TriggerSync/Messages/TriggerInitialStateMessage.cs index 5c371c79..bbf30193 100644 --- a/QSB/TriggerSync/Messages/TriggerInitialStateMessage.cs +++ b/QSB/TriggerSync/Messages/TriggerInitialStateMessage.cs @@ -17,12 +17,12 @@ public class TriggerInitialStateMessage : QSBWorldObjectMessage : WorldObject, IQSBTrigger QSBPlayerManager.OnRemovePlayer -= OnPlayerLeave; } + private void OnPlayerLeave(PlayerInfo player) => Exit(player); + public override void SendInitialState(uint to) => ((IQSBTrigger)this).SendMessage(new TriggerInitialStateMessage(Occupants) { To = to }); @@ -71,14 +73,6 @@ public abstract class QSBTrigger : WorldObject, IQSBTrigger } } - private void OnPlayerLeave(PlayerInfo player) - { - if (Occupants.Contains(player)) - { - Exit(player); - } - } - public void Enter(PlayerInfo player) { if (!Occupants.SafeAdd(player)) @@ -108,4 +102,4 @@ public abstract class QSBTrigger : WorldObject, IQSBTrigger /// called when a player exits this trigger or leaves the game /// protected virtual void OnExit(PlayerInfo player) { } -} \ No newline at end of file +} diff --git a/QSB/Utility/DebugActions.cs b/QSB/Utility/DebugActions.cs index bbb9e4c7..66d1476a 100644 --- a/QSB/Utility/DebugActions.cs +++ b/QSB/Utility/DebugActions.cs @@ -1,10 +1,12 @@ -using QSB.ItemSync.WorldObjects.Items; +using OWML.Common; +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; using System.Linq; using UnityEngine; using UnityEngine.InputSystem; @@ -13,6 +15,8 @@ namespace QSB.Utility; public class DebugActions : MonoBehaviour, IAddComponentOnStart { + public static Type WorldObjectSelection; + private static void GoToVessel() { var spawnPoint = GameObject.Find("Spawn_Vessel").GetComponent(); @@ -38,6 +42,49 @@ public class DebugActions : MonoBehaviour, IAddComponentOnStart private void Awake() => enabled = QSBCore.DebugSettings.DebugMode; private int _otherPlayerToTeleportTo; + private int _backTimer; + private int _forwardTimer; + + private const int UpdatesUntilScroll = 30; + private const int UpdatesBetweenScroll = 5; + + private static void GoForwardOneObject() + { + var allWorldObjects = typeof(IWorldObject).GetDerivedTypes().ToArray(); + if (WorldObjectSelection == null) + { + WorldObjectSelection = allWorldObjects.First(); + return; + } + + var index = Array.IndexOf(allWorldObjects, WorldObjectSelection) + 1; + + if (index == allWorldObjects.Length) + { + index = 0; + } + + WorldObjectSelection = allWorldObjects[index]; + } + + private static void GoBackOneObject() + { + var allWorldObjects = typeof(IWorldObject).GetDerivedTypes().ToArray(); + if (WorldObjectSelection == null) + { + WorldObjectSelection = allWorldObjects.Last(); + return; + } + + var index = Array.IndexOf(allWorldObjects, WorldObjectSelection) - 1; + + if (index < 0) + { + index = allWorldObjects.Length - 1; + } + + WorldObjectSelection = allWorldObjects[index]; + } public void Update() { @@ -46,6 +93,57 @@ public class DebugActions : MonoBehaviour, IAddComponentOnStart return; } + if (Keyboard.current[Key.Comma].isPressed && Keyboard.current[Key.Period].isPressed) + { + WorldObjectSelection = null; + } + else if (Keyboard.current[Key.Comma].wasPressedThisFrame) + { + GoBackOneObject(); + } + else if (Keyboard.current[Key.Period].wasPressedThisFrame) + { + GoForwardOneObject(); + } + else + { + if (Keyboard.current[Key.Comma].isPressed) + { + _backTimer++; + + if (_backTimer >= UpdatesUntilScroll) + { + if (_backTimer == UpdatesUntilScroll + UpdatesBetweenScroll) + { + _backTimer = UpdatesUntilScroll; + GoBackOneObject(); + } + } + } + else + { + _backTimer = 0; + } + + if (Keyboard.current[Key.Period].isPressed) + { + _forwardTimer++; + + if (_forwardTimer >= UpdatesUntilScroll) + { + if (_forwardTimer == UpdatesUntilScroll + UpdatesBetweenScroll) + { + _forwardTimer = UpdatesUntilScroll; + GoForwardOneObject(); + } + } + } + else + { + _forwardTimer = 0; + } + } + if (Keyboard.current[Key.Numpad1].wasPressedThisFrame) { var otherPlayers = QSBPlayerManager.PlayerList.Where(x => !x.IsLocalPlayer).ToList(); @@ -119,11 +217,17 @@ public class DebugActions : MonoBehaviour, IAddComponentOnStart if (Keyboard.current[Key.Numpad7].wasPressedThisFrame) { GoToVessel(); + InsertWarpCore(); } if (Keyboard.current[Key.Numpad8].wasPressedThisFrame) { - InsertWarpCore(); + var player = new PlayerInfo(QSBPlayerManager.LocalPlayer.TransformSync); + QSBPlayerManager.PlayerList.SafeAdd(player); + QSBPlayerManager.OnAddPlayer?.Invoke(player); + DebugLog.DebugWrite($"Create Player : {player}", MessageType.Info); + + JoinLeaveSingularity.Create(player, true); } if (Keyboard.current[Key.Numpad9].wasPressedThisFrame) diff --git a/QSB/Utility/DebugGUI.cs b/QSB/Utility/DebugGUI.cs index 9740a88c..5d237c00 100644 --- a/QSB/Utility/DebugGUI.cs +++ b/QSB/Utility/DebugGUI.cs @@ -133,6 +133,8 @@ internal class DebugGUI : MonoBehaviour, IAddComponentOnStart WriteLine(1, $"TimeLoop IsTimeFlowing : {TimeLoop.IsTimeFlowing()}"); WriteLine(1, $"TimeLoop IsTimeLoopEnabled : {TimeLoop.IsTimeLoopEnabled()}"); } + + WriteLine(1, $"Selected WorldObject : {(DebugActions.WorldObjectSelection == null ? "All" : DebugActions.WorldObjectSelection.Name)}"); } #endregion @@ -142,23 +144,26 @@ internal class DebugGUI : MonoBehaviour, IAddComponentOnStart WriteLine(2, "Player data :"); foreach (var player in QSBPlayerManager.PlayerList) { - WriteLine(2, player.ToString()); + WriteLine(2, player.ToString(), Color.cyan); WriteLine(2, $"State : {player.State}"); WriteLine(2, $"Eye State : {player.EyeState}"); WriteLine(2, $"Dead : {player.IsDead}"); - WriteLine(2, $"Visible : {player.Visible}"); WriteLine(2, $"Ready : {player.IsReady}"); WriteLine(2, $"Suited Up : {player.SuitedUp}"); + WriteLine(2, $"In Suited Up State : {player.AnimationSync.InSuitedUpState}"); WriteLine(2, $"InDreamWorld : {player.InDreamWorld}"); - if (player.IsReady && QSBWorldSync.AllObjectsReady) { WriteLine(2, $"Illuminated : {player.LightSensor.IsIlluminated()}"); var singleLightSensor = (SingleLightSensor)player.LightSensor; - foreach (var item in singleLightSensor._lightSources) + // will be null for remote player light sensors + if (singleLightSensor._lightSources != null) { - WriteLine(2, $"- {item.GetLightSourceType()}"); + foreach (var item in singleLightSensor._lightSources) + { + WriteLine(2, $"- {item.GetLightSourceType()}"); + } } var networkTransform = player.TransformSync; @@ -258,7 +263,7 @@ internal class DebugGUI : MonoBehaviour, IAddComponentOnStart } else { - WriteLine(4, $"- LANTERN NULL", Color.red); + WriteLine(4, "- LANTERN NULL", Color.red); } var playerCamera = player.player.Camera; @@ -271,7 +276,7 @@ internal class DebugGUI : MonoBehaviour, IAddComponentOnStart } else { - WriteLine(4, $"- CAMERA NULL", Color.red); + WriteLine(4, "- CAMERA NULL", Color.red); } } } @@ -323,7 +328,11 @@ internal class DebugGUI : MonoBehaviour, IAddComponentOnStart { if (QSBCore.DebugSettings.DrawLabels) { - foreach (var obj in QSBWorldSync.GetWorldObjects()) + var list = DebugActions.WorldObjectSelection == null + ? QSBWorldSync.GetWorldObjects() + : QSBWorldSync.GetWorldObjects(DebugActions.WorldObjectSelection); + + foreach (var obj in list) { if (obj.ShouldDisplayDebug()) { @@ -349,7 +358,11 @@ internal class DebugGUI : MonoBehaviour, IAddComponentOnStart { if (QSBCore.DebugSettings.DrawLines) { - foreach (var obj in QSBWorldSync.GetWorldObjects()) + var list = DebugActions.WorldObjectSelection == null + ? QSBWorldSync.GetWorldObjects() + : QSBWorldSync.GetWorldObjects(DebugActions.WorldObjectSelection); + + foreach (var obj in list) { if (obj.ShouldDisplayDebug()) { diff --git a/QSB/Utility/DebugLog.cs b/QSB/Utility/DebugLog.cs index 2c5351aa..5f01ec8e 100644 --- a/QSB/Utility/DebugLog.cs +++ b/QSB/Utility/DebugLog.cs @@ -1,9 +1,9 @@ using OWML.Common; using OWML.Logging; -using QSB.WorldSync; using System.Diagnostics; using System.Linq; -using UnityEngine; +using System.Reflection; +using System.Runtime.CompilerServices; namespace QSB.Utility; @@ -19,7 +19,7 @@ public static class DebugLog message = $"[{ProcessInstanceId}] " + message; } - QSBCore.Helper.Console.WriteLine(message, type, GetCallingType(new StackTrace())); + QSBCore.Helper.Console.WriteLine(message, type, GetCallingType()); } public static void ToHud(string message) @@ -44,7 +44,7 @@ public static class DebugLog if (QSBCore.Helper == null) { // yes i know this is only meant for OWML, but it's useful as a backup - ModConsole.OwmlConsole.WriteLine(message, type, GetCallingType(new StackTrace())); + ModConsole.OwmlConsole.WriteLine(message, type, GetCallingType()); return; } @@ -54,8 +54,10 @@ public static class DebugLog } } - private static string GetCallingType(StackTrace frame) => - frame.GetFrames()! - .Select(x => x.GetMethod().DeclaringType!.Name) - .First(x => x != nameof(DebugLog)); + private static string GetCallingType() => + new StackTrace(2) // skip this function and calling function + .GetFrames()! + .Select(x => x.GetMethod().DeclaringType!) + .First(x => x != typeof(DebugLog) && !x.IsDefined(typeof(CompilerGeneratedAttribute))) + .Name; } diff --git a/QSB/Utility/DeterministicManager.cs b/QSB/Utility/DeterministicManager.cs index f190d585..c1982a1d 100644 --- a/QSB/Utility/DeterministicManager.cs +++ b/QSB/Utility/DeterministicManager.cs @@ -8,6 +8,9 @@ using UnityEngine; namespace QSB.Utility; +/// +/// TODO make this only do cache clearing on pre scene load when HOSTING instead of just all the time +/// public static class DeterministicManager { private static readonly Harmony _harmony = new(typeof(DeterministicManager).FullName); diff --git a/QSB/Utility/LinkedWorldObject/Extensions.cs b/QSB/Utility/LinkedWorldObject/ILinkedWorldObject_Extensions.cs similarity index 64% rename from QSB/Utility/LinkedWorldObject/Extensions.cs rename to QSB/Utility/LinkedWorldObject/ILinkedWorldObject_Extensions.cs index 97b0b69d..c3ab2893 100644 --- a/QSB/Utility/LinkedWorldObject/Extensions.cs +++ b/QSB/Utility/LinkedWorldObject/ILinkedWorldObject_Extensions.cs @@ -5,27 +5,27 @@ using UnityEngine; namespace QSB.Utility.LinkedWorldObject; -public static class Extensions +public static class ILinkedWorldObject_Extensions { /// /// link a world object and a network behaviour /// - public static void LinkTo(this ILinkedWorldObject worldObject, ILinkedNetworkBehaviour networkBehaviour) + public static void LinkTo(this ILinkedWorldObject @this, ILinkedNetworkBehaviour networkBehaviour) { - worldObject.SetNetworkBehaviour((NetworkBehaviour)networkBehaviour); - networkBehaviour.SetWorldObject(worldObject); + @this.SetNetworkBehaviour((NetworkBehaviour)networkBehaviour); + networkBehaviour.SetWorldObject(@this); } /// /// link a world object and network object, then spawn it. /// (host only) /// - public static void SpawnLinked(this ILinkedWorldObject worldObject, GameObject prefab, bool spawnWithServerAuthority) + public static void SpawnLinked(this ILinkedWorldObject @this, GameObject prefab, bool spawnWithServerAuthority) { var go = Object.Instantiate(prefab); var networkBehaviour = go.GetComponent(); - worldObject.LinkTo(networkBehaviour); + @this.LinkTo(networkBehaviour); if (spawnWithServerAuthority) { @@ -41,6 +41,6 @@ public static class Extensions /// wait for a world object to be linked. /// (non host only) /// - public static async UniTask WaitForLink(this ILinkedWorldObject worldObject, CancellationToken ct) => - await UniTask.WaitUntil(() => worldObject.NetworkBehaviour, cancellationToken: ct); + public static async UniTask WaitForLink(this ILinkedWorldObject @this, CancellationToken ct) => + await UniTask.WaitUntil(() => @this.NetworkBehaviour, cancellationToken: ct); } diff --git a/QSB/Utility/Messages/DebugChangeSceneMessage.cs b/QSB/Utility/Messages/DebugChangeSceneMessage.cs index bd0d2aca..4a70571c 100644 --- a/QSB/Utility/Messages/DebugChangeSceneMessage.cs +++ b/QSB/Utility/Messages/DebugChangeSceneMessage.cs @@ -12,8 +12,7 @@ public class DebugChangeSceneMessage : QSBMessage { if (Data) { - PlayerData._currentGameSave.warpedToTheEye = false; - PlayerData.SaveCurrentGame(); + PlayerData.SaveEyeCompletion(); LoadManager.LoadSceneAsync(OWScene.SolarSystem, true, LoadManager.FadeType.ToBlack); } else diff --git a/QSB/Utility/Messages/DebugTriggerSupernovaMessage.cs b/QSB/Utility/Messages/DebugTriggerSupernovaMessage.cs index 121965e1..1dbba547 100644 --- a/QSB/Utility/Messages/DebugTriggerSupernovaMessage.cs +++ b/QSB/Utility/Messages/DebugTriggerSupernovaMessage.cs @@ -1,9 +1,17 @@ using QSB.Messaging; +using QSB.Patches; namespace QSB.Utility.Messages; public class DebugTriggerSupernovaMessage : QSBMessage { public override void OnReceiveLocal() => OnReceiveRemote(); - public override void OnReceiveRemote() => TimeLoop.SetSecondsRemaining(0); -} \ No newline at end of file + + public override void OnReceiveRemote() + { + PlayerData.SaveLoopCount(2); + TimeLoop.SetTimeLoopEnabled(true); + TimeLoop._isTimeFlowing = true; + QSBPatch.RemoteCall(() => TimeLoop.SetSecondsRemaining(0)); + } +} diff --git a/QSB/WorldSync/QSBOWRigidbody.cs b/QSB/WorldSync/QSBOWRigidbody.cs index 9d35cb48..323d342b 100644 --- a/QSB/WorldSync/QSBOWRigidbody.cs +++ b/QSB/WorldSync/QSBOWRigidbody.cs @@ -5,7 +5,5 @@ /// internal class QSBOWRigidbody : WorldObject { - public override void SendInitialState(uint to) { } - public override bool ShouldDisplayDebug() => false; } diff --git a/QSB/WorldSync/QSBWorldSync.cs b/QSB/WorldSync/QSBWorldSync.cs index 5d93be2d..5d788f70 100644 --- a/QSB/WorldSync/QSBWorldSync.cs +++ b/QSB/WorldSync/QSBWorldSync.cs @@ -187,7 +187,7 @@ public static class QSBWorldSync if (QSBCore.IsInMultiplayer && loadScene.IsUniverseScene()) { // So objects have time to be deleted, made, whatever - // I.E. wait until Start has been called + // i.e. wait until Start has been called Delay.RunNextFrame(() => BuildWorldObjects(loadScene).Forget()); } }; @@ -233,6 +233,9 @@ public static class QSBWorldSync where TWorldObject : IWorldObject => WorldObjects.OfType(); + public static IEnumerable GetWorldObjects(Type type) + => WorldObjects.Where(type.IsInstanceOfType); + public static TWorldObject GetWorldObject(this int objectId) where TWorldObject : IWorldObject { diff --git a/QSB/WorldSync/WorldObject.cs b/QSB/WorldSync/WorldObject.cs index a9a324a3..b01aa699 100644 --- a/QSB/WorldSync/WorldObject.cs +++ b/QSB/WorldSync/WorldObject.cs @@ -19,5 +19,5 @@ public abstract class WorldObject : IWorldObject public virtual string ReturnLabel() => ToString(); public virtual void DisplayLines() { } - public abstract void SendInitialState(uint to); + public virtual void SendInitialState(uint to) { } } \ No newline at end of file diff --git a/QSB/manifest.json b/QSB/manifest.json index 6de3460a..256b8626 100644 --- a/QSB/manifest.json +++ b/QSB/manifest.json @@ -7,7 +7,7 @@ "body": "- Disable *all* other mods. (Can heavily affect performance)\n- Make sure you are not running any other network-intensive applications." }, "uniqueName": "Raicuparta.QuantumSpaceBuddies", - "version": "0.20.2", + "version": "0.21.0", "owmlVersion": "2.5.2", "dependencies": [ "_nebula.MenuFramework", "JohnCorby.VanillaFix" ], "pathsToPreserve": [ "debugsettings.json", "storage.json" ] diff --git a/README.md b/README.md index 4611bf02..05d88efe 100644 --- a/README.md +++ b/README.md @@ -181,10 +181,10 @@ The template for this file is this : ### Contributers -- [ShoosGun](https://github.com/ShoosGun) - [Chris Yeninas](https://github.com/PhantomGamers) - Help with project files and GitHub workflows. - [Tlya](https://github.com/Tllya) - Russian translation. - [Xen](https://github.com/xen-42) - French translation. +- [ShoosGun](https://github.com/ShoosGun) - Portuguese translation. ### Special Thanks - Thanks to Logan Ver Hoef for help with the game code, and for helping make the damn game in the first place. diff --git a/TRANSLATING.md b/TRANSLATING.md index d8a4b7fb..bd1f8c92 100644 --- a/TRANSLATING.md +++ b/TRANSLATING.md @@ -8,13 +8,13 @@ QSB can only be translated to the languages Outer Wilds supports - so if you don - English - French - Russian +- Portuguese (Brazil) ### Un-translated languages : - Spanish (Latin American) - German - Italian - Polish -- Portuguese (Brazil) - Japanese - Chinese (Simplified) - Korean